diff --git a/.github/ISSUE_TEMPLATE/file.md b/.github/ISSUE_TEMPLATE/file.md new file mode 100644 index 000000000..3430d7e01 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/file.md @@ -0,0 +1,5 @@ +--- +name: "package:file" +about: "Create a bug or file a feature request against package:file." +labels: "package:file" +--- diff --git a/.github/labeler.yml b/.github/labeler.yml index 02386d004..00496d87e 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -16,6 +16,14 @@ - changed-files: - any-glob-to-any-file: 'pkgs/extension_discovery/**' +'package:file': + - changed-files: + - any-glob-to-any-file: 'pkgs/file/**' + +'package:file_testing': + - changed-files: + - any-glob-to-any-file: 'pkgs/file_testing/**' + 'package:graphs': - changed-files: - any-glob-to-any-file: 'pkgs/graphs/**' diff --git a/.github/workflows/file.yml b/.github/workflows/file.yml new file mode 100644 index 000000000..06ce285d4 --- /dev/null +++ b/.github/workflows/file.yml @@ -0,0 +1,66 @@ +name: package:file +permissions: read-all + +on: + # Run on PRs and pushes to the default branch. + push: + branches: [ main ] + paths: + - '.github/workflows/file.yml' + - 'pkgs/file/**' + - 'pkgs/file_testing/**' + pull_request: + branches: [ main ] + paths: + - '.github/workflows/file.yml' + - 'pkgs/file/**' + - 'pkgs/file_testing/**' + schedule: + - cron: "0 0 * * 0" + +jobs: + correctness: + runs-on: ubuntu-latest + strategy: + matrix: + package: [file, file_testing] + + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + with: + sdk: dev + + - name: Install ${{ matrix.package }} dependencies + working-directory: pkgs/${{ matrix.package }} + run: dart pub get + + - name: Verify formatting in ${{ matrix.package }} + working-directory: pkgs/${{ matrix.package }} + run: dart format --output=none --set-exit-if-changed . + + - name: Analyze package ${{ matrix.package }} source + working-directory: pkgs/${{ matrix.package }} + run: dart analyze --fatal-infos + + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + package: [file] + os: [ubuntu-latest, macos-latest, windows-latest] + sdk: [stable, dev] + + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + with: + sdk: ${{ matrix.sdk }} + + - name: Install ${{ matrix.package }} dependencies + working-directory: pkgs/${{ matrix.package }} + run: dart pub get + + - name: Run ${{ matrix.package }} Tests + working-directory: pkgs/${{ matrix.package }} + run: dart pub run test -j1 diff --git a/README.md b/README.md index 147618489..153d34c78 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ don't naturally belong to other topic monorepos (like | [cli_config](pkgs/cli_config/) | A library to take config values from configuration files, CLI arguments, and environment variables. | [![pub package](https://img.shields.io/pub/v/cli_config.svg)](https://pub.dev/packages/cli_config) | | [coverage](pkgs/coverage/) | Coverage data manipulation and formatting. | [![pub package](https://img.shields.io/pub/v/coverage.svg)](https://pub.dev/packages/coverage) | | [extension_discovery](pkgs/extension_discovery/) | A convention and utilities for package extension discovery. | [![pub package](https://img.shields.io/pub/v/extension_discovery.svg)](https://pub.dev/packages/extension_discovery) | +| [file](pkgs/file/) | A pluggable, mockable file system abstraction for Dart. | [![pub package](https://img.shields.io/pub/v/file.svg)](https://pub.dev/packages/file) | +| [file_testing](pkgs/file_testing/) | Testing utilities for package:file (published but unlisted). | [![pub package](https://img.shields.io/pub/v/file_testing.svg)](https://pub.dev/packages/file_testing) | | [graphs](pkgs/graphs/) | Graph algorithms that operate on graphs in any representation | [![pub package](https://img.shields.io/pub/v/graphs.svg)](https://pub.dev/packages/graphs) | | [mime](pkgs/mime/) | Utilities for handling media (MIME) types. | [![pub package](https://img.shields.io/pub/v/mime.svg)](https://pub.dev/packages/mime) | | [oauth2](pkgs/oauth2/) | A client library for authenticatingand making requests via OAuth2. | [![pub package](https://img.shields.io/pub/v/oauth2.svg)](https://pub.dev/packages/oauth2) | diff --git a/pkgs/file/.gitignore b/pkgs/file/.gitignore new file mode 100644 index 000000000..ddfdca160 --- /dev/null +++ b/pkgs/file/.gitignore @@ -0,0 +1,5 @@ +.dart_tool/ +.packages +.pub/ +build/ +pubspec.lock diff --git a/pkgs/file/CHANGELOG.md b/pkgs/file/CHANGELOG.md new file mode 100644 index 000000000..a0e588a9a --- /dev/null +++ b/pkgs/file/CHANGELOG.md @@ -0,0 +1,239 @@ +## 7.0.1-wip + +## 7.0.0 + +* Dart 3 fixes for class modifiers. +* `MemoryFileSystem` now treats empty paths as non-existent. +* Fix `FileSystem.isLink`/`FileSystem.isLinkSync` to not follow symbolic links. +* Make the return type of `MemoryFile.openRead` and `_ChrootFile.openRead` again + match the return type from `dart:io`. + +## 6.1.4 + +* Populate the pubspec `repository` field. + +## 6.1.3 + +* In classes that implement `File` methods `create`, `createSync` now take `bool exclusive = true` parameter. No functional changes. + +## 6.1.2 + +* `MemoryFileSystem` now provides `opHandle`s for exists operations. + +## 6.1.1 + +* `MemoryFile` now provides `opHandle`s for copy and open operations. + +## 6.1.0 + +* Reading invalid UTF8 with the `MemoryFileSystem` now correctly throws a `FileSystemException` instead of a `FormatError`. +* `MemoryFileSystem` now provides an `opHandle` to inspect read/write operations. +* `MemoryFileSystem` now creates the temporary directory before returning in `createTemp`/`createTempSync`. + +## 6.0.1 + +* Fix sound type error in memory backend when reading non-existent `MemoryDirectory`. + +## 6.0.0 + +* First stable null safe release. + +## 6.0.0-nullsafety.4 + +* Update upper bound of SDK constraint. + +## 6.0.0-nullsafety.3 + +* Update upper bound of SDK constraint. + +## 6.0.0-nullsafety.2 + +* Make `ForwardingFile.openRead`'s return type again match the return type from + `dart:io`. +* Remove some unnecessary `Uint8List` conversions in `ForwardingFile`. + +## 6.0.0-nullsafety.1 + +* Update to null safety. +* Remove record/replay functionality. +* Made `MemoryRandomAccessFile` and `MemoryFile.openWrite` handle the file + being removed or renamed while open. +* Fixed incorrect formatting in `NoMatchingInvocationError.toString()`. +* Fixed more test flakiness. +* Enabled more tests. +* Internal cleanup. +* Remove implicit dynamic in preparation for null safety. +* Remove dependency on Intl. + +## 5.2.1 + +* systemTemp directories created by `MemoryFileSystem` will allot names + based on the file system instance instead of globally. +* `MemoryFile.readAsLines()`/`readAsLinesSync()` no longer treat a final newline + in the file as the start of a new, empty line. +* `RecordingFile.readAsLine()`/`readAsLinesSync()` now always record a final + newline. +* `MemoryFile.flush()` now returns `Future` instead of `Future`. +* Fixed some test flakiness. +* Enabled more tests. +* Internal cleanup. + +## 5.2.0 + +* Added a `MemoryRandomAccessFile` class and implemented + `MemoryFile.open()`/`openSync()`. + +## 5.1.0 + +* Added a new `MemoryFileSystem` constructor to use a test clock + +## 5.0.10 + +* Added example + +## 5.0.9 + +* Fix lints for project health + +## 5.0.8 + +* Return `Uint8List` rather than `List`. + +## 5.0.7 + +* Dart 2 fixes for `RecordingProxyMixin` and `ReplayProxyMixin`. + +## 5.0.6 + +* Dart 2 fixes for `RecordingFile.open()` + +## 5.0.5 + +* Dart 2 fixes + +## 5.0.4 + +* Update SDK constraint to 2.0.0-dev.67.0, remove workaround in + recording_proxy_mixin.dart. +* Fix usage within Dart 2 runtime mode in Dart 2.0.0-dev.61.0 and later. +* Relax constraints on `package:test` + +## 5.0.3 + +* Update `package:test` dependency to 1.0 + +## 5.0.2 + +* Declare compatibility with Dart 2 stable + +## 5.0.1 + +* Remove upper case constants +* Update SDK constraint to 2.0.0-dev.54.0. + +## 5.0.0 + +* Moved `testing` library into a dedicated `package:file_testing` so that + libraries don't need to take on a transitive dependency on `package:test` + in order to use `package:file`. + +## 4.0.1 + +* General library cleanup +* Add `style` support in `MemoryFileSystem`, so that callers can choose to + have a memory file system with windows-like paths. [#68] + (https://github.com/google/file.dart/issues/68) + +## 4.0.0 + +* Change method signature for `RecordingRandomAccessFile._close` to return a + `Future` instead of `Future`. This follows a change in + dart:io, Dart SDK `2.0.0-dev.40`. + +## 3.0.0 + +* Import `dart:io` unconditionally. More recent Dart SDK revisions allow + `dart:io` to be imported in a browser context, though if methods are actually + invoked, they will fail. This matches well with `package:file`, where users + can use the `memory` library and get in-memory implementations of the + `dart:io` interfaces. +* Bump minimum Dart SDK to `1.24.0` + +## 2.3.7 + +* Fix Dart 2 error. + +## 2.3.6 + +* Relax sdk upper bound constraint to '<2.0.0' to allow 'edge' dart sdk use. + +## 2.3.5 + +* Fix internal use of a cast which fails on Dart 2.0 . + +## 2.3.4 + +* Bumped maximum Dart SDK version to 2.0.0-dev.infinity + +## 2.3.3 + +* Relaxes version requirements on `package:intl` + +## 2.3.2 + +* Fixed `FileSystem.directory(Uri)`, `FileSystem.file(Uri)`, and + `FileSystem.link(Uri)` to consult the file system's path context when + converting the URI to a file path rather than using `Uri.toFilePath()`. + +## 2.3.1 + +* Fixed `MemoryFileSystem` to make `File.writeAs...()` update the last modified + time of the file. + +## 2.3.0 + +* Added the following convenience methods in `Directory`: + * `Directory.childDirectory(String basename)` + * `Directory.childFile(String basename)` + * `Directory.childLink(String basename)` + +## 2.2.0 + +* Added `ErrorCodes` class, which holds errno values. + +## 2.1.0 + +* Add support for new `dart:io` API methods added in Dart SDK 1.23 + +## 2.0.1 + +* Minor doc updates + +## 2.0.0 + +* Improved `toString` implementations in file system entity classes +* Added `ForwardingFileSystem` and associated forwarding classes to the + main `file` library +* Removed `FileSystem.pathSeparator`, and added a more comprehensive + `FileSystem.path` property +* Added `FileSystemEntity.basename` and `FileSystemEntity.dirname` +* Added the `record_replay` library +* Added the `testing` library + +## 1.0.1 + +* Added `FileSystem.systemTempDirectory` +* Added the ability to pass `Uri` and `FileSystemEntity` types to + `FileSystem.directory()`, `FileSystem.file()`, and `FileSystem.link()` +* Added `FileSystem.pathSeparator` + +## 1.0.0 + +* Unified interface to match dart:io API +* Local file system implementation +* In-memory file system implementation +* Chroot file system implementation + +## 0.1.0 + +* Initial version diff --git a/pkgs/file/LICENSE b/pkgs/file/LICENSE new file mode 100644 index 000000000..076334f7a --- /dev/null +++ b/pkgs/file/LICENSE @@ -0,0 +1,26 @@ +Copyright 2017, the Dart project authors. All rights reserved. +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 Inc. 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. \ No newline at end of file diff --git a/pkgs/file/README.md b/pkgs/file/README.md new file mode 100644 index 000000000..a63d6e647 --- /dev/null +++ b/pkgs/file/README.md @@ -0,0 +1,44 @@ +[![package:file](https://github.com/dart-lang/tools/actions/workflows/file.yml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/file.yml) +[![pub package](https://img.shields.io/pub/v/file.svg)](https://pub.dev/packages/file) +[![package publisher](https://img.shields.io/pub/publisher/file.svg)](https://pub.dev/packages/file/publisher) + +A generic file system abstraction for Dart. + +## Features + +Like `dart:io`, `package:file` supplies a rich Dart-idiomatic API for accessing +a file system. + +Unlike `dart:io`, `package:file`: + +- Can be used to implement custom file systems. +- Comes with an in-memory implementation out-of-the-box, making it super-easy to + test code that works with the file system. +- Allows using multiple file systems simultaneously. A file system is a + first-class object. Instantiate however many you want and use them all. + +## Usage + +Implement your own custom file system: + +```dart +import 'package:file/file.dart'; + +class FooBarFileSystem implements FileSystem { ... } +``` + +Use the in-memory file system: + +```dart +import 'package:file/memory.dart'; + +var fs = MemoryFileSystem(); +``` + +Use the local file system (requires dart:io access): + +```dart +import 'package:file/local.dart'; + +var fs = const LocalFileSystem(); +``` diff --git a/pkgs/file/analysis_options.yaml b/pkgs/file/analysis_options.yaml new file mode 100644 index 000000000..8fbd2e443 --- /dev/null +++ b/pkgs/file/analysis_options.yaml @@ -0,0 +1,6 @@ +include: package:lints/recommended.yaml + +analyzer: + errors: + # Allow having TODOs in the code + todo: ignore diff --git a/pkgs/file/example/main.dart b/pkgs/file/example/main.dart new file mode 100644 index 000000000..7ca0bc73f --- /dev/null +++ b/pkgs/file/example/main.dart @@ -0,0 +1,14 @@ +// 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:file/file.dart'; +import 'package:file/memory.dart'; + +Future main() async { + final FileSystem fs = MemoryFileSystem(); + final Directory tmp = await fs.systemTempDirectory.createTemp('example_'); + final File outputFile = tmp.childFile('output'); + await outputFile.writeAsString('Hello world!'); + print(outputFile.readAsStringSync()); +} diff --git a/pkgs/file/lib/chroot.dart b/pkgs/file/lib/chroot.dart new file mode 100644 index 000000000..56d2bd5d7 --- /dev/null +++ b/pkgs/file/lib/chroot.dart @@ -0,0 +1,6 @@ +// Copyright (c) 2017, 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 file system that provides a view into _another_ `FileSystem` via a path. +export 'src/backends/chroot.dart'; diff --git a/pkgs/file/lib/file.dart b/pkgs/file/lib/file.dart new file mode 100644 index 000000000..cdde9fedd --- /dev/null +++ b/pkgs/file/lib/file.dart @@ -0,0 +1,8 @@ +// Copyright (c) 2017, 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. + +/// Core interfaces containing the abstract `FileSystem` interface definition +/// and all associated types used by `FileSystem`. +export 'src/forwarding.dart'; +export 'src/interface.dart'; diff --git a/pkgs/file/lib/local.dart b/pkgs/file/lib/local.dart new file mode 100644 index 000000000..74f506e36 --- /dev/null +++ b/pkgs/file/lib/local.dart @@ -0,0 +1,7 @@ +// Copyright (c) 2017, 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 local file system implementation. This relies on the use of `dart:io` +/// and is thus not suitable for use in the browser. +export 'src/backends/local.dart'; diff --git a/pkgs/file/lib/memory.dart b/pkgs/file/lib/memory.dart new file mode 100644 index 000000000..c5705eff9 --- /dev/null +++ b/pkgs/file/lib/memory.dart @@ -0,0 +1,8 @@ +// Copyright (c) 2017, 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 implementation of `FileSystem` that exists entirely in memory with an +/// internal representation loosely based on the Filesystem Hierarchy Standard. +export 'src/backends/memory.dart'; +export 'src/backends/memory/operations.dart'; diff --git a/pkgs/file/lib/src/backends/chroot.dart b/pkgs/file/lib/src/backends/chroot.dart new file mode 100644 index 000000000..6082e808c --- /dev/null +++ b/pkgs/file/lib/src/backends/chroot.dart @@ -0,0 +1,20 @@ +// Copyright (c) 2017, 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. + +library file.src.backends.chroot; + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:file/file.dart'; +import 'package:file/src/common.dart' as common; +import 'package:file/src/io.dart' as io; +import 'package:path/path.dart' as p; + +part 'chroot/chroot_directory.dart'; +part 'chroot/chroot_file.dart'; +part 'chroot/chroot_file_system.dart'; +part 'chroot/chroot_file_system_entity.dart'; +part 'chroot/chroot_link.dart'; +part 'chroot/chroot_random_access_file.dart'; diff --git a/pkgs/file/lib/src/backends/chroot/chroot_directory.dart b/pkgs/file/lib/src/backends/chroot/chroot_directory.dart new file mode 100644 index 000000000..8fec7b198 --- /dev/null +++ b/pkgs/file/lib/src/backends/chroot/chroot_directory.dart @@ -0,0 +1,176 @@ +// Copyright (c) 2017, 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. + +part of file.src.backends.chroot; + +class _ChrootDirectory extends _ChrootFileSystemEntity + with ForwardingDirectory, common.DirectoryAddOnsMixin { + _ChrootDirectory(ChrootFileSystem fs, String path) : super(fs, path); + + factory _ChrootDirectory.wrapped( + ChrootFileSystem fs, + Directory delegate, { + bool relative = false, + }) { + String localPath = fs._local(delegate.path, relative: relative); + return _ChrootDirectory(fs, localPath); + } + + @override + FileSystemEntityType get expectedType => FileSystemEntityType.directory; + + @override + io.Directory _rawDelegate(String path) => fileSystem.delegate.directory(path); + + @override + Uri get uri => Uri.directory(path); + + @override + Future rename(String newPath) async { + if (_isLink) { + if (await fileSystem.type(path) != expectedType) { + throw common.notADirectory(path); + } + FileSystemEntityType type = await fileSystem.type(newPath); + if (type != FileSystemEntityType.notFound) { + if (type != expectedType) { + throw common.notADirectory(newPath); + } + if (!(await fileSystem + .directory(newPath) + .list(followLinks: false) + .isEmpty)) { + throw common.directoryNotEmpty(newPath); + } + } + String target = await fileSystem.link(path).target(); + await fileSystem.link(path).delete(); + await fileSystem.link(newPath).create(target); + return fileSystem.directory(newPath); + } else { + return wrap(await getDelegate(followLinks: true) + .rename(fileSystem._real(newPath))); + } + } + + @override + Directory renameSync(String newPath) { + if (_isLink) { + if (fileSystem.typeSync(path) != expectedType) { + throw common.notADirectory(path); + } + FileSystemEntityType type = fileSystem.typeSync(newPath); + if (type != FileSystemEntityType.notFound) { + if (type != expectedType) { + throw common.notADirectory(newPath); + } + if (fileSystem + .directory(newPath) + .listSync(followLinks: false) + .isNotEmpty) { + throw common.directoryNotEmpty(newPath); + } + } + String target = fileSystem.link(path).targetSync(); + fileSystem.link(path).deleteSync(); + fileSystem.link(newPath).createSync(target); + return fileSystem.directory(newPath); + } else { + return wrap( + getDelegate(followLinks: true).renameSync(fileSystem._real(newPath))); + } + } + + @override + Directory get absolute => _ChrootDirectory(fileSystem, _absolutePath); + + @override + Directory get parent { + try { + return wrapDirectory(delegate.parent); + } on _ChrootJailException { + return this; + } + } + + @override + Future create({bool recursive = false}) async { + if (_isLink) { + switch (await fileSystem.type(path)) { + case FileSystemEntityType.notFound: + throw common.noSuchFileOrDirectory(path); + case FileSystemEntityType.file: + throw common.fileExists(path); + case FileSystemEntityType.directory: + // Nothing to do. + return this; + default: + throw AssertionError(); + } + } else { + return wrap(await delegate.create(recursive: recursive)); + } + } + + @override + void createSync({bool recursive = false}) { + if (_isLink) { + switch (fileSystem.typeSync(path)) { + case FileSystemEntityType.notFound: + throw common.noSuchFileOrDirectory(path); + case FileSystemEntityType.file: + throw common.fileExists(path); + case FileSystemEntityType.directory: + // Nothing to do. + return; + default: + throw AssertionError(); + } + } else { + delegate.createSync(recursive: recursive); + } + } + + @override + Stream list({ + bool recursive = false, + bool followLinks = true, + }) { + Directory delegate = this.delegate as Directory; + String dirname = delegate.path; + return delegate + .list(recursive: recursive, followLinks: followLinks) + .map((io.FileSystemEntity entity) => _denormalize(entity, dirname)); + } + + @override + List listSync({ + bool recursive = false, + bool followLinks = true, + }) { + Directory delegate = this.delegate as Directory; + String dirname = delegate.path; + return delegate + .listSync(recursive: recursive, followLinks: followLinks) + .map((io.FileSystemEntity entity) => _denormalize(entity, dirname)) + .toList(); + } + + FileSystemEntity _denormalize(io.FileSystemEntity entity, String dirname) { + p.Context ctx = fileSystem.path; + String relativePart = ctx.relative(entity.path, from: dirname); + String entityPath = ctx.join(path, relativePart); + if (entity is io.File) { + return _ChrootFile(fileSystem, entityPath); + } else if (entity is io.Directory) { + return _ChrootDirectory(fileSystem, entityPath); + } else if (entity is io.Link) { + return _ChrootLink(fileSystem, entityPath); + } + throw FileSystemException('Unsupported type: $entity', entity.path); + } + + @override + String toString() => "ChrootDirectory: '$path'"; +} diff --git a/pkgs/file/lib/src/backends/chroot/chroot_file.dart b/pkgs/file/lib/src/backends/chroot/chroot_file.dart new file mode 100644 index 000000000..4b67bc1f6 --- /dev/null +++ b/pkgs/file/lib/src/backends/chroot/chroot_file.dart @@ -0,0 +1,339 @@ +// Copyright (c) 2017, 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. + +part of file.src.backends.chroot; + +typedef _SetupCallback = dynamic Function(); + +class _ChrootFile extends _ChrootFileSystemEntity + with ForwardingFile { + _ChrootFile(ChrootFileSystem fs, String path) : super(fs, path); + + factory _ChrootFile.wrapped( + ChrootFileSystem fs, + io.File delegate, { + bool relative = false, + }) { + String localPath = fs._local(delegate.path, relative: relative); + return _ChrootFile(fs, localPath); + } + + @override + FileSystemEntityType get expectedType => FileSystemEntityType.file; + + @override + io.File _rawDelegate(String path) => fileSystem.delegate.file(path); + + @override + Future rename(String newPath) async { + _SetupCallback setUp = () async {}; + + if (await fileSystem.type(newPath, followLinks: false) == + FileSystemEntityType.link) { + // The delegate file system will ensure that the link target references + // an actual file before allowing the rename, but we want the link target + // to be resolved with respect to this file system. Thus, we perform that + // validation here instead. + switch (await fileSystem.type(newPath)) { + case FileSystemEntityType.file: + case FileSystemEntityType.notFound: + // Validation passed; delete the link to keep the delegate file + // system's validation from getting in the way. + setUp = () async { + await fileSystem.link(newPath).delete(); + }; + break; + case FileSystemEntityType.directory: + throw common.isADirectory(newPath); + default: + // Should never happen. + throw AssertionError(); + } + } + + if (_isLink) { + switch (await fileSystem.type(path)) { + case FileSystemEntityType.notFound: + throw common.noSuchFileOrDirectory(path); + case FileSystemEntityType.directory: + throw common.isADirectory(path); + case FileSystemEntityType.file: + await setUp(); + await fileSystem.delegate + .link(fileSystem._real(path)) + .rename(fileSystem._real(newPath)); + return _ChrootFile(fileSystem, newPath); + default: + throw AssertionError(); + } + } else { + await setUp(); + return wrap(await delegate.rename(fileSystem._real(newPath))); + } + } + + @override + File renameSync(String newPath) { + _SetupCallback setUp = () {}; + + if (fileSystem.typeSync(newPath, followLinks: false) == + FileSystemEntityType.link) { + // The delegate file system will ensure that the link target references + // an actual file before allowing the rename, but we want the link target + // to be resolved with respect to this file system. Thus, we perform that + // validation here instead. + switch (fileSystem.typeSync(newPath)) { + case FileSystemEntityType.file: + case FileSystemEntityType.notFound: + // Validation passed; delete the link to keep the delegate file + // system's validation from getting in the way. + setUp = () { + fileSystem.link(newPath).deleteSync(); + }; + break; + case FileSystemEntityType.directory: + throw common.isADirectory(newPath); + default: + // Should never happen. + throw AssertionError(); + } + } + + if (_isLink) { + switch (fileSystem.typeSync(path)) { + case FileSystemEntityType.notFound: + throw common.noSuchFileOrDirectory(path); + case FileSystemEntityType.directory: + throw common.isADirectory(path); + case FileSystemEntityType.file: + setUp(); + fileSystem.delegate + .link(fileSystem._real(path)) + .renameSync(fileSystem._real(newPath)); + return _ChrootFile(fileSystem, newPath); + default: + throw AssertionError(); + } + } else { + setUp(); + return wrap(delegate.renameSync(fileSystem._real(newPath))); + } + } + + @override + File get absolute => _ChrootFile(fileSystem, _absolutePath); + + @override + Future create({bool recursive = false, bool exclusive = false}) async { + String path = fileSystem._resolve( + this.path, + followLinks: false, + notFound: recursive ? _NotFoundBehavior.mkdir : _NotFoundBehavior.allow, + ); + + String real() => fileSystem._real(path, resolve: false); + Future type() => + fileSystem.delegate.type(real(), followLinks: false); + + if (await type() == FileSystemEntityType.link) { + path = fileSystem._resolve(p.basename(path), + from: p.dirname(path), notFound: _NotFoundBehavior.allowAtTail); + switch (await type()) { + case FileSystemEntityType.notFound: + await _rawDelegate(real()).create(); + return this; + case FileSystemEntityType.file: + // Nothing to do. + return this; + case FileSystemEntityType.directory: + throw common.isADirectory(path); + default: + throw AssertionError(); + } + } else { + return wrap(await _rawDelegate(real()).create()); + } + } + + @override + void createSync({bool recursive = false, bool exclusive = false}) { + String path = fileSystem._resolve( + this.path, + followLinks: false, + notFound: recursive ? _NotFoundBehavior.mkdir : _NotFoundBehavior.allow, + ); + + String real() => fileSystem._real(path, resolve: false); + FileSystemEntityType type() => + fileSystem.delegate.typeSync(real(), followLinks: false); + + if (type() == FileSystemEntityType.link) { + path = fileSystem._resolve(p.basename(path), + from: p.dirname(path), notFound: _NotFoundBehavior.allowAtTail); + switch (type()) { + case FileSystemEntityType.notFound: + _rawDelegate(real()).createSync(); + return; + case FileSystemEntityType.file: + // Nothing to do. + return; + case FileSystemEntityType.directory: + throw common.isADirectory(path); + default: + throw AssertionError(); + } + } else { + _rawDelegate(real()).createSync(); + } + } + + @override + Future copy(String newPath) async { + return wrap(await getDelegate(followLinks: true) + .copy(fileSystem._real(newPath, followLinks: true))); + } + + @override + File copySync(String newPath) { + return wrap(getDelegate(followLinks: true) + .copySync(fileSystem._real(newPath, followLinks: true))); + } + + @override + Future length() => getDelegate(followLinks: true).length(); + + @override + int lengthSync() => getDelegate(followLinks: true).lengthSync(); + + @override + Future lastAccessed() => + getDelegate(followLinks: true).lastAccessed(); + + @override + DateTime lastAccessedSync() => + getDelegate(followLinks: true).lastAccessedSync(); + + @override + Future setLastAccessed(DateTime time) => + getDelegate(followLinks: true).setLastAccessed(time); + + @override + void setLastAccessedSync(DateTime time) => + getDelegate(followLinks: true).setLastAccessedSync(time); + + @override + Future lastModified() => + getDelegate(followLinks: true).lastModified(); + + @override + DateTime lastModifiedSync() => + getDelegate(followLinks: true).lastModifiedSync(); + + @override + Future setLastModified(DateTime time) => + getDelegate(followLinks: true).setLastModified(time); + + @override + void setLastModifiedSync(DateTime time) => + getDelegate(followLinks: true).setLastModifiedSync(time); + + @override + Future open({ + FileMode mode = FileMode.read, + }) async => + _ChrootRandomAccessFile( + path, await getDelegate(followLinks: true).open(mode: mode)); + + @override + RandomAccessFile openSync({FileMode mode = FileMode.read}) => + _ChrootRandomAccessFile( + path, getDelegate(followLinks: true).openSync(mode: mode)); + + @override + Stream> openRead([int? start, int? end]) => + getDelegate(followLinks: true).openRead(start, end); + + @override + IOSink openWrite({ + FileMode mode = FileMode.write, + Encoding encoding = utf8, + }) => + getDelegate(followLinks: true).openWrite(mode: mode, encoding: encoding); + + @override + Future readAsBytes() => + getDelegate(followLinks: true).readAsBytes(); + + @override + Uint8List readAsBytesSync() => + getDelegate(followLinks: true).readAsBytesSync(); + + @override + Future readAsString({Encoding encoding = utf8}) => + getDelegate(followLinks: true).readAsString(encoding: encoding); + + @override + String readAsStringSync({Encoding encoding = utf8}) => + getDelegate(followLinks: true).readAsStringSync(encoding: encoding); + + @override + Future> readAsLines({Encoding encoding = utf8}) => + getDelegate(followLinks: true).readAsLines(encoding: encoding); + + @override + List readAsLinesSync({Encoding encoding = utf8}) => + getDelegate(followLinks: true).readAsLinesSync(encoding: encoding); + + @override + Future writeAsBytes( + List bytes, { + FileMode mode = FileMode.write, + bool flush = false, + }) async => + wrap(await getDelegate(followLinks: true).writeAsBytes( + bytes, + mode: mode, + flush: flush, + )); + + @override + void writeAsBytesSync( + List bytes, { + FileMode mode = FileMode.write, + bool flush = false, + }) => + getDelegate(followLinks: true) + .writeAsBytesSync(bytes, mode: mode, flush: flush); + + @override + Future writeAsString( + String contents, { + FileMode mode = FileMode.write, + Encoding encoding = utf8, + bool flush = false, + }) async => + wrap(await getDelegate(followLinks: true).writeAsString( + contents, + mode: mode, + encoding: encoding, + flush: flush, + )); + + @override + void writeAsStringSync( + String contents, { + FileMode mode = FileMode.write, + Encoding encoding = utf8, + bool flush = false, + }) => + getDelegate(followLinks: true).writeAsStringSync( + contents, + mode: mode, + encoding: encoding, + flush: flush, + ); + + @override + String toString() => "ChrootFile: '$path'"; +} diff --git a/pkgs/file/lib/src/backends/chroot/chroot_file_system.dart b/pkgs/file/lib/src/backends/chroot/chroot_file_system.dart new file mode 100644 index 000000000..6889c987b --- /dev/null +++ b/pkgs/file/lib/src/backends/chroot/chroot_file_system.dart @@ -0,0 +1,391 @@ +// Copyright (c) 2017, 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. + +part of file.src.backends.chroot; + +const String _thisDir = '.'; +const String _parentDir = '..'; + +/// File system that provides a view into _another_ [FileSystem] via a path. +/// +/// This is similar in concept to the `chroot` operation in Linux operating +/// systems. Such a modified file system cannot name or access files outside of +/// the designated directory tree. +/// +/// ## Example use: +/// ```dart +/// // Create a "file system" where the root directory is /tmp/some-dir. +/// var fs = ChrootFileSystem(existingFileSystem, '/tmp/some-dir'); +/// ``` +/// +/// **Notes on usage**: +/// +/// * This file system maintains its _own_ [currentDirectory], distinct from +/// that of the underlying file system, and new instances automatically start +/// at the root (i.e. `/`). +/// +/// * This file system does _not_ leverage any underlying OS system calls (such +/// as `chroot` itself), so the developer needs to take care to not assume any +/// more of a secure environment than is actually provided. For instance, the +/// underlying system is available via the [delegate] - which underscores this +/// file system is intended to be a convenient abstraction, not a security +/// measure. +/// +/// * This file system _necessarily_ carries certain performance overhead due +/// to the fact that symbolic links are resolved manually (not delegated). +class ChrootFileSystem extends FileSystem { + /// Creates a new file system backed by [root] path in [delegate] file system. + /// + /// **NOTE**: [root] must be a _canonicalized_ path; see [p.canonicalize]. + ChrootFileSystem(this.delegate, this.root) { + if (root != delegate.path.canonicalize(root)) { + throw ArgumentError.value(root, 'root', 'Must be canonical path'); + } + _cwd = _localRoot; + } + + /// Underlying file system. + final FileSystem delegate; + + /// Directory in [delegate] file system that is treated as the root here. + final String root; + + String? _systemTemp; + + /// Path to the synthetic current working directory in this file system. + late String _cwd; + + /// Gets the root path, as seen by entities in this file system. + String get _localRoot => delegate.path.rootPrefix(root); + + @override + Directory directory(dynamic path) => _ChrootDirectory(this, getPath(path)); + + @override + File file(dynamic path) => _ChrootFile(this, getPath(path)); + + @override + Link link(dynamic path) => _ChrootLink(this, getPath(path)); + + @override + p.Context get path => p.Context(style: delegate.path.style, current: _cwd); + + /// Gets the system temp directory. This directory will be created on-demand + /// in the local root of the file system. Once created, its location is fixed + /// for the life of the process. + @override + Directory get systemTempDirectory { + _systemTemp ??= directory(_localRoot).createTempSync('.tmp_').path; + return directory(_systemTemp)..createSync(); + } + + /// Creates a directory object pointing to the current working directory. + /// + /// **NOTE** This does _not_ proxy to the underlying file system's current + /// directory in any way; the state of this file system's current directory + /// is local to this file system. + @override + Directory get currentDirectory => directory(_cwd); + + /// Sets the current working directory to the specified [path]. + /// + /// **NOTE** This does _not_ proxy to the underlying file system's current + /// directory in any way; the state of this file system's current directory + /// is local to this file system. + /// Gets the path context for this file system given the current working dir. + + @override + set currentDirectory(dynamic path) { + String value; + if (path is io.Directory) { + value = path.path; + } else if (path is String) { + value = path; + } else { + throw ArgumentError('Invalid type for "path": ${path?.runtimeType}'); + } + + value = _resolve(value, notFound: _NotFoundBehavior.throwError); + String realPath = _real(value, resolve: false); + switch (delegate.typeSync(realPath, followLinks: false)) { + case FileSystemEntityType.directory: + break; + case FileSystemEntityType.notFound: + throw common.noSuchFileOrDirectory(path as String); + default: + throw common.notADirectory(path as String); + } + assert(() { + p.Context ctx = delegate.path; + return ctx.isAbsolute(value) && value == ctx.canonicalize(value); + }()); + _cwd = value; + } + + @override + Future stat(String path) { + try { + path = _resolve(path); + } on FileSystemException { + return Future.value(const _NotFoundFileStat()); + } + return delegate.stat(_real(path, resolve: false)); + } + + @override + FileStat statSync(String path) { + try { + path = _resolve(path); + } on FileSystemException { + return const _NotFoundFileStat(); + } + return delegate.statSync(_real(path, resolve: false)); + } + + @override + Future identical(String path1, String path2) => delegate.identical( + _real(_resolve(path1, followLinks: false)), + _real(_resolve(path2, followLinks: false)), + ); + + @override + bool identicalSync(String path1, String path2) => delegate.identicalSync( + _real(_resolve(path1, followLinks: false)), + _real(_resolve(path2, followLinks: false)), + ); + + @override + bool get isWatchSupported => false; + + @override + Future type(String path, {bool followLinks = true}) { + String realPath; + try { + realPath = _real(path, followLinks: followLinks); + } on FileSystemException { + return Future.value(FileSystemEntityType.notFound); + } + return delegate.type(realPath, followLinks: false); + } + + @override + FileSystemEntityType typeSync(String path, {bool followLinks = true}) { + String realPath; + try { + realPath = _real(path, followLinks: followLinks); + } on FileSystemException { + return FileSystemEntityType.notFound; + } + return delegate.typeSync(realPath, followLinks: false); + } + + /// Converts a [realPath] in the underlying file system to a local path here. + /// + /// If [relative] is set to `true`, then the resulting path will be relative + /// to [currentDirectory], otherwise the resulting path will be absolute. + /// + /// An exception is thrown if the path is outside of this file system's root + /// directory unless [keepInJail] is true, in which case this will instead + /// return the path of the root of this file system. + String _local( + String realPath, { + bool relative = false, + bool keepInJail = false, + }) { + assert(path.isAbsolute(realPath)); + if (!realPath.startsWith(root)) { + if (keepInJail) { + return _localRoot; + } + throw _ChrootJailException(); + } + // TODO(tvolkert): See if _context.relative() works here + String result = realPath.substring(root.length); + if (result.isEmpty) { + result = _localRoot; + } + if (relative) { + assert(result.startsWith(_cwd)); + result = path.relative(result, from: _cwd); + } + return result; + } + + /// Converts [localPath] in this file system to the real path in the delegate. + /// + /// The returned path will always be absolute. + /// + /// If [resolve] is true, symbolic links will be resolved in the local file + /// system _before_ converting the path to the delegate file system's + /// namespace, and if the tail element of the path is a symbolic link, it will + /// only be resolved if [followLinks] is true (where-as symbolic links found + /// in the middle of the path will always be resolved). + String _real( + String localPath, { + bool resolve = true, + bool followLinks = false, + }) { + if (resolve) { + localPath = _resolve(localPath, followLinks: followLinks); + } else { + assert(path.isAbsolute(localPath)); + } + return '$root$localPath'; + } + + /// Resolves symbolic links on [path] and returns the resulting resolved path. + /// + /// The return value will always be an absolute path; if [path] is relative + /// it will be interpreted relative to [from] (or [currentDirectory] if + /// `null`). + /// + /// If the tail element is a symbolic link, then the link will be resolved + /// only if [followLinks] is `true`. Symbolic links found in the middle of + /// the path will always be resolved. + /// + /// If the path cannot be resolved, and [notFound] is: + /// - [_NotFoundBehavior.throwError]: a [FileSystemException] is thrown. + /// - [_NotFoundBehavior.mkdir]: the path will be created as needed. + /// - [_NotFoundBehavior.allowAtTail]: a [FileSystemException] is thrown, + /// unless only the *tail* path element cannot be resolved, in which case + /// the resolution will halt at the tail element, and the partially + /// resolved path will be returned. + /// - [_NotFoundBehavior.allow] (the default), the resolution will halt and + /// the partially resolved path will be returned. + String _resolve( + String path, { + String? from, + bool followLinks = true, + _NotFoundBehavior notFound = _NotFoundBehavior.allow, + }) { + if (path.isEmpty) { + throw common.noSuchFileOrDirectory(path); + } + + p.Context ctx = this.path; + String root = _localRoot; + List parts, ledger; + if (ctx.isAbsolute(path)) { + parts = ctx.split(path).sublist(1); + ledger = []; + } else { + from ??= _cwd; + assert(ctx.isAbsolute(from)); + parts = ctx.split(path); + ledger = ctx.split(from).sublist(1); + } + + String getCurrentPath() => root + ctx.joinAll(ledger); + Set breadcrumbs = {}; + while (parts.isNotEmpty) { + String segment = parts.removeAt(0); + if (segment == _thisDir) { + continue; + } else if (segment == _parentDir) { + if (ledger.isNotEmpty) { + ledger.removeLast(); + } + continue; + } + + ledger.add(segment); + String currentPath = getCurrentPath(); + String realPath = _real(currentPath, resolve: false); + + switch (delegate.typeSync(realPath, followLinks: false)) { + case FileSystemEntityType.directory: + breadcrumbs.clear(); + break; + case FileSystemEntityType.file: + breadcrumbs.clear(); + if (parts.isNotEmpty) { + throw common.notADirectory(currentPath); + } + break; + case FileSystemEntityType.notFound: + String returnEarly() { + ledger.addAll(parts); + return getCurrentPath(); + } + + switch (notFound) { + case _NotFoundBehavior.mkdir: + if (parts.isNotEmpty) { + delegate.directory(realPath).createSync(); + } + break; + case _NotFoundBehavior.allow: + return returnEarly(); + case _NotFoundBehavior.allowAtTail: + if (parts.isEmpty) { + return returnEarly(); + } + throw common.noSuchFileOrDirectory(path); + case _NotFoundBehavior.throwError: + throw common.noSuchFileOrDirectory(path); + } + break; + case FileSystemEntityType.link: + if (parts.isEmpty && !followLinks) { + break; + } + if (!breadcrumbs.add(currentPath)) { + throw common.tooManyLevelsOfSymbolicLinks(path); + } + String target = delegate.link(realPath).targetSync(); + if (ctx.isAbsolute(target)) { + ledger.clear(); + parts.insertAll(0, ctx.split(target).sublist(1)); + } else { + ledger.removeLast(); + parts.insertAll(0, ctx.split(target)); + } + break; + default: + throw AssertionError(); + } + } + + return getCurrentPath(); + } +} + +/// Thrown when a path is encountered that exists outside of the root path. +class _ChrootJailException implements IOException {} + +/// What to do when `NOT_FOUND` paths are encountered while resolving. +enum _NotFoundBehavior { + allow, + allowAtTail, + throwError, + mkdir, +} + +/// A [FileStat] representing a `NOT_FOUND` entity. +class _NotFoundFileStat implements FileStat { + const _NotFoundFileStat(); + + static final DateTime _empty = DateTime(0); + + @override + DateTime get changed => _empty; + + @override + DateTime get modified => _empty; + + @override + DateTime get accessed => _empty; + + @override + FileSystemEntityType get type => FileSystemEntityType.notFound; + + @override + int get mode => 0; + + @override + int get size => -1; + + @override + String modeString() => '---------'; +} diff --git a/pkgs/file/lib/src/backends/chroot/chroot_file_system_entity.dart b/pkgs/file/lib/src/backends/chroot/chroot_file_system_entity.dart new file mode 100644 index 000000000..8e859ace8 --- /dev/null +++ b/pkgs/file/lib/src/backends/chroot/chroot_file_system_entity.dart @@ -0,0 +1,169 @@ +// Copyright (c) 2017, 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. + +part of file.src.backends.chroot; + +abstract class _ChrootFileSystemEntity extends ForwardingFileSystemEntity { + _ChrootFileSystemEntity(this.fileSystem, this.path); + + @override + final ChrootFileSystem fileSystem; + + @override + final String path; + + @override + String get dirname => fileSystem.path.dirname(path); + + @override + String get basename => fileSystem.path.basename(path); + + @override + D get delegate => getDelegate(); + + /// Gets the delegate file system entity in the underlying file system that + /// corresponds to this entity's local file system path. + /// + /// If [followLinks] is true and this entity's path references a symbolic + /// link, then the path of the delegate entity will reference the ultimate + /// target of that symbolic link. Symbolic links in the middle of the path + /// will always be resolved in the delegate entity's path. + D getDelegate({bool followLinks = false}) => + _rawDelegate(fileSystem._real(path, followLinks: followLinks)); + + /// Returns the expected type of this entity, which may differ from the type + /// of the entity that's found at the path specified by this entity. + FileSystemEntityType get expectedType; + + /// Returns a delegate entity at the specified [realPath] (the path in the + /// underlying file system). + D _rawDelegate(String realPath); + + /// Gets the path of this entity as an absolute path (unchanged if the + /// entity already specifies an absolute path). + String get _absolutePath => fileSystem.path.absolute(path); + + /// Tells whether this entity's path references a symbolic link. + bool get _isLink => + fileSystem.typeSync(path, followLinks: false) == + FileSystemEntityType.link; + + @override + Directory wrapDirectory(io.Directory delegate) => + _ChrootDirectory.wrapped(fileSystem, delegate as Directory, + relative: !isAbsolute); + + @override + File wrapFile(io.File delegate) => + _ChrootFile.wrapped(fileSystem, delegate, relative: !isAbsolute); + + @override + Link wrapLink(io.Link delegate) => + _ChrootLink.wrapped(fileSystem, delegate, relative: !isAbsolute); + + @override + Uri get uri => Uri.file(path); + + @override + Future exists() => getDelegate(followLinks: true).exists(); + + @override + bool existsSync() => getDelegate(followLinks: true).existsSync(); + + @override + Future resolveSymbolicLinks() async => resolveSymbolicLinksSync(); + + @override + String resolveSymbolicLinksSync() => + fileSystem._resolve(path, notFound: _NotFoundBehavior.throwError); + + @override + Future stat() { + D delegate; + try { + delegate = getDelegate(followLinks: true); + } on FileSystemException { + return Future.value(const _NotFoundFileStat()); + } + return delegate.stat(); + } + + @override + FileStat statSync() { + D delegate; + try { + delegate = getDelegate(followLinks: true); + } on FileSystemException { + return const _NotFoundFileStat(); + } + return delegate.statSync(); + } + + @override + Future delete({bool recursive = false}) async { + String path = fileSystem._resolve(this.path, + followLinks: false, notFound: _NotFoundBehavior.throwError); + + String real(String path) => fileSystem._real(path, resolve: false); + Future type(String path) => + fileSystem.delegate.type(real(path), followLinks: false); + + if (await type(path) == FileSystemEntityType.link) { + if (expectedType == FileSystemEntityType.link) { + await fileSystem.delegate.link(real(path)).delete(); + } else { + String resolvedPath = fileSystem._resolve(p.basename(path), + from: p.dirname(path), notFound: _NotFoundBehavior.allowAtTail); + if (!recursive && await type(resolvedPath) != expectedType) { + throw expectedType == FileSystemEntityType.file + ? common.isADirectory(path) + : common.notADirectory(path); + } + await fileSystem.delegate.link(real(path)).delete(); + } + return this as T; + } else { + return wrap( + await _rawDelegate(real(path)).delete(recursive: recursive) as D); + } + } + + @override + void deleteSync({bool recursive = false}) { + String path = fileSystem._resolve(this.path, + followLinks: false, notFound: _NotFoundBehavior.throwError); + + String real(String path) => fileSystem._real(path, resolve: false); + FileSystemEntityType type(String path) => + fileSystem.delegate.typeSync(real(path), followLinks: false); + + if (type(path) == FileSystemEntityType.link) { + if (expectedType == FileSystemEntityType.link) { + fileSystem.delegate.link(real(path)).deleteSync(); + } else { + String resolvedPath = fileSystem._resolve(p.basename(path), + from: p.dirname(path), notFound: _NotFoundBehavior.allowAtTail); + if (!recursive && type(resolvedPath) != expectedType) { + throw expectedType == FileSystemEntityType.file + ? common.isADirectory(path) + : common.notADirectory(path); + } + fileSystem.delegate.link(real(path)).deleteSync(); + } + } else { + _rawDelegate(real(path)).deleteSync(recursive: recursive); + } + } + + @override + Stream watch({ + int events = FileSystemEvent.all, + bool recursive = false, + }) => + throw UnsupportedError('watch is not supported on ChrootFileSystem'); + + @override + bool get isAbsolute => fileSystem.path.isAbsolute(path); +} diff --git a/pkgs/file/lib/src/backends/chroot/chroot_link.dart b/pkgs/file/lib/src/backends/chroot/chroot_link.dart new file mode 100644 index 000000000..acbeda60c --- /dev/null +++ b/pkgs/file/lib/src/backends/chroot/chroot_link.dart @@ -0,0 +1,47 @@ +// Copyright (c) 2017, 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. + +part of file.src.backends.chroot; + +class _ChrootLink extends _ChrootFileSystemEntity + with ForwardingLink { + _ChrootLink(ChrootFileSystem fs, String path) : super(fs, path); + + factory _ChrootLink.wrapped( + ChrootFileSystem fs, + io.Link delegate, { + bool relative = false, + }) { + String localPath = fs._local(delegate.path, relative: relative); + return _ChrootLink(fs, localPath); + } + + @override + Future exists() => delegate.exists(); + + @override + bool existsSync() => delegate.existsSync(); + + @override + Future rename(String newPath) async { + return wrap(await delegate.rename(fileSystem._real(newPath))); + } + + @override + Link renameSync(String newPath) { + return wrap(delegate.renameSync(fileSystem._real(newPath))); + } + + @override + FileSystemEntityType get expectedType => FileSystemEntityType.link; + + @override + io.Link _rawDelegate(String path) => fileSystem.delegate.link(path); + + @override + Link get absolute => _ChrootLink(fileSystem, _absolutePath); + + @override + String toString() => "ChrootLink: '$path'"; +} diff --git a/pkgs/file/lib/src/backends/chroot/chroot_random_access_file.dart b/pkgs/file/lib/src/backends/chroot/chroot_random_access_file.dart new file mode 100644 index 000000000..4105ac807 --- /dev/null +++ b/pkgs/file/lib/src/backends/chroot/chroot_random_access_file.dart @@ -0,0 +1,15 @@ +// Copyright (c) 2020, 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. + +part of file.src.backends.chroot; + +class _ChrootRandomAccessFile with ForwardingRandomAccessFile { + _ChrootRandomAccessFile(this.path, this.delegate); + + @override + final io.RandomAccessFile delegate; + + @override + final String path; +} diff --git a/pkgs/file/lib/src/backends/local.dart b/pkgs/file/lib/src/backends/local.dart new file mode 100644 index 000000000..1e9224169 --- /dev/null +++ b/pkgs/file/lib/src/backends/local.dart @@ -0,0 +1,5 @@ +// Copyright (c) 2017, 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. + +export 'local/local_file_system.dart' show LocalFileSystem; diff --git a/pkgs/file/lib/src/backends/local/local_directory.dart b/pkgs/file/lib/src/backends/local/local_directory.dart new file mode 100644 index 000000000..e23e68fe0 --- /dev/null +++ b/pkgs/file/lib/src/backends/local/local_directory.dart @@ -0,0 +1,20 @@ +// Copyright (c) 2017, 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:file/file.dart'; +import 'package:file/src/common.dart' as common; +import 'package:file/src/io.dart' as io; + +import 'local_file_system_entity.dart'; + +/// [Directory] implementation that forwards all calls to `dart:io`. +class LocalDirectory extends LocalFileSystemEntity + with ForwardingDirectory, common.DirectoryAddOnsMixin { + /// Instantiates a new [LocalDirectory] tied to the specified file system + /// and delegating to the specified [delegate]. + LocalDirectory(FileSystem fs, io.Directory delegate) : super(fs, delegate); + + @override + String toString() => "LocalDirectory: '$path'"; +} diff --git a/pkgs/file/lib/src/backends/local/local_file.dart b/pkgs/file/lib/src/backends/local/local_file.dart new file mode 100644 index 000000000..36293ba51 --- /dev/null +++ b/pkgs/file/lib/src/backends/local/local_file.dart @@ -0,0 +1,19 @@ +// Copyright (c) 2017, 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:file/file.dart'; +import 'package:file/src/io.dart' as io; + +import 'local_file_system_entity.dart'; + +/// [File] implementation that forwards all calls to `dart:io`. +class LocalFile extends LocalFileSystemEntity + with ForwardingFile { + /// Instantiates a new [LocalFile] tied to the specified file system + /// and delegating to the specified [delegate]. + LocalFile(FileSystem fs, io.File delegate) : super(fs, delegate); + + @override + String toString() => "LocalFile: '$path'"; +} diff --git a/pkgs/file/lib/src/backends/local/local_file_system.dart b/pkgs/file/lib/src/backends/local/local_file_system.dart new file mode 100644 index 000000000..635998e10 --- /dev/null +++ b/pkgs/file/lib/src/backends/local/local_file_system.dart @@ -0,0 +1,72 @@ +// Copyright (c) 2017, 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:file/src/io.dart' as io; +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'local_directory.dart'; +import 'local_file.dart'; +import 'local_link.dart'; + +/// A wrapper implementation around `dart:io`'s implementation. +/// +/// Since this implementation of the [FileSystem] interface delegates to +/// `dart:io`, is is not suitable for use in the browser. +class LocalFileSystem extends FileSystem { + /// Creates a new `LocalFileSystem`. + const LocalFileSystem(); + + @override + Directory directory(dynamic path) => + LocalDirectory(this, io.Directory(getPath(path))); + + @override + File file(dynamic path) => LocalFile(this, io.File(getPath(path))); + + @override + Link link(dynamic path) => LocalLink(this, io.Link(getPath(path))); + + @override + p.Context get path => p.Context(); + + /// Gets the directory provided by the operating system for creating temporary + /// files and directories in. The location of the system temp directory is + /// platform-dependent, and may be set by an environment variable. + @override + Directory get systemTempDirectory => + LocalDirectory(this, io.Directory.systemTemp); + + @override + Directory get currentDirectory => directory(io.Directory.current.path); + + @override + set currentDirectory(dynamic path) => io.Directory.current = path; + + @override + Future stat(String path) => io.FileStat.stat(path); + + @override + io.FileStat statSync(String path) => io.FileStat.statSync(path); + + @override + Future identical(String path1, String path2) => + io.FileSystemEntity.identical(path1, path2); + + @override + bool identicalSync(String path1, String path2) => + io.FileSystemEntity.identicalSync(path1, path2); + + @override + bool get isWatchSupported => io.FileSystemEntity.isWatchSupported; + + @override + Future type(String path, + {bool followLinks = true}) => + io.FileSystemEntity.type(path, followLinks: followLinks); + + @override + io.FileSystemEntityType typeSync(String path, {bool followLinks = true}) => + io.FileSystemEntity.typeSync(path, followLinks: followLinks); +} diff --git a/pkgs/file/lib/src/backends/local/local_file_system_entity.dart b/pkgs/file/lib/src/backends/local/local_file_system_entity.dart new file mode 100644 index 000000000..ca4617b01 --- /dev/null +++ b/pkgs/file/lib/src/backends/local/local_file_system_entity.dart @@ -0,0 +1,40 @@ +// Copyright (c) 2017, 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:file/file.dart'; +import 'package:file/src/io.dart' as io; + +import 'local_directory.dart'; +import 'local_file.dart'; +import 'local_link.dart'; + +/// [FileSystemEntity] implementation that forwards all calls to `dart:io`. +abstract class LocalFileSystemEntity extends ForwardingFileSystemEntity { + /// Instantiates a new [LocalFileSystemEntity] tied to the specified file + /// system and delegating to the specified [delegate]. + LocalFileSystemEntity(this.fileSystem, this.delegate); + + @override + final FileSystem fileSystem; + + @override + final D delegate; + + @override + String get dirname => fileSystem.path.dirname(path); + + @override + String get basename => fileSystem.path.basename(path); + + @override + Directory wrapDirectory(io.Directory delegate) => + LocalDirectory(fileSystem, delegate); + + @override + File wrapFile(io.File delegate) => LocalFile(fileSystem, delegate); + + @override + Link wrapLink(io.Link delegate) => LocalLink(fileSystem, delegate); +} diff --git a/pkgs/file/lib/src/backends/local/local_link.dart b/pkgs/file/lib/src/backends/local/local_link.dart new file mode 100644 index 000000000..fc67d5e88 --- /dev/null +++ b/pkgs/file/lib/src/backends/local/local_link.dart @@ -0,0 +1,19 @@ +// Copyright (c) 2017, 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:file/file.dart'; +import 'package:file/src/io.dart' as io; + +import 'local_file_system_entity.dart'; + +/// [Link] implementation that forwards all calls to `dart:io`. +class LocalLink extends LocalFileSystemEntity + with ForwardingLink { + /// Instantiates a new [LocalLink] tied to the specified file system + /// and delegating to the specified [delegate]. + LocalLink(FileSystem fs, io.Link delegate) : super(fs, delegate); + + @override + String toString() => "LocalLink: '$path'"; +} diff --git a/pkgs/file/lib/src/backends/memory.dart b/pkgs/file/lib/src/backends/memory.dart new file mode 100644 index 000000000..428bc5418 --- /dev/null +++ b/pkgs/file/lib/src/backends/memory.dart @@ -0,0 +1,6 @@ +// Copyright (c) 2017, 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. + +export 'memory/memory_file_system.dart' show MemoryFileSystem; +export 'memory/style.dart' show FileSystemStyle, StyleableFileSystem; diff --git a/pkgs/file/lib/src/backends/memory/clock.dart b/pkgs/file/lib/src/backends/memory/clock.dart new file mode 100644 index 000000000..98d5434f9 --- /dev/null +++ b/pkgs/file/lib/src/backends/memory/clock.dart @@ -0,0 +1,52 @@ +// 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. + +/// Interface describing clocks used by the [MemoryFileSystem]. +/// +/// The [MemoryFileSystem] uses a clock to determine the modification times of +/// files that are created in that file system. +abstract class Clock { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const Clock(); + + /// A real-time clock. + /// + /// Uses [DateTime.now] to reflect the actual time reported by the operating + /// system. + const factory Clock.realTime() = _RealtimeClock; + + /// A monotonically-increasing test clock. + /// + /// Each time [now] is called, the time increases by one minute. + /// + /// The `start` argument can be used to set the seed time for the clock. + /// The first value will be that time plus one minute. + /// By default, `start` is midnight on the first of January, 2000. + factory Clock.monotonicTest() = _MonotonicTestClock; + + /// Returns the value of the clock. + DateTime get now; +} + +class _RealtimeClock extends Clock { + const _RealtimeClock(); + + @override + DateTime get now => DateTime.now(); +} + +class _MonotonicTestClock extends Clock { + _MonotonicTestClock({ + DateTime? start, + }) : _current = start ?? DateTime(2000); + + DateTime _current; + + @override + DateTime get now { + _current = _current.add(const Duration(minutes: 1)); + return _current; + } +} diff --git a/pkgs/file/lib/src/backends/memory/common.dart b/pkgs/file/lib/src/backends/memory/common.dart new file mode 100644 index 000000000..80e3c3851 --- /dev/null +++ b/pkgs/file/lib/src/backends/memory/common.dart @@ -0,0 +1,15 @@ +// 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:file/src/common.dart' as common; + +/// Generates a path to use in error messages. +typedef PathGenerator = dynamic Function(); + +/// Throws a `FileSystemException` if [object] is null. +void checkExists(Object? object, PathGenerator path) { + if (object == null) { + throw common.noSuchFileOrDirectory(path() as String); + } +} diff --git a/pkgs/file/lib/src/backends/memory/memory_directory.dart b/pkgs/file/lib/src/backends/memory/memory_directory.dart new file mode 100644 index 000000000..95fe54247 --- /dev/null +++ b/pkgs/file/lib/src/backends/memory/memory_directory.dart @@ -0,0 +1,184 @@ +// Copyright (c) 2017, 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:file/file.dart'; +import 'package:file/src/common.dart' as common; +import 'package:file/src/io.dart' as io; +import 'package:meta/meta.dart'; + +import 'common.dart'; +import 'memory_file.dart'; +import 'memory_file_system_entity.dart'; +import 'memory_link.dart'; +import 'node.dart'; +import 'operations.dart'; +import 'style.dart'; +import 'utils.dart' as utils; + +// Tracks a unique name for system temp directories, per filesystem +// instance. +final Expando _systemTempCounter = Expando(); + +/// Internal implementation of [Directory]. +class MemoryDirectory extends MemoryFileSystemEntity + with common.DirectoryAddOnsMixin + implements Directory { + /// Instantiates a new [MemoryDirectory]. + MemoryDirectory(NodeBasedFileSystem fileSystem, String path) + : super(fileSystem, path); + + @override + io.FileSystemEntityType get expectedType => io.FileSystemEntityType.directory; + + @override + Uri get uri { + return Uri.directory(path, + windows: fileSystem.style == FileSystemStyle.windows); + } + + @override + bool existsSync() { + fileSystem.opHandle.call(path, FileSystemOp.exists); + return backingOrNull?.stat.type == expectedType; + } + + @override + Future create({bool recursive = false}) async { + createSync(recursive: recursive); + return this; + } + + @override + void createSync({bool recursive = false}) { + fileSystem.opHandle(path, FileSystemOp.create); + Node? node = internalCreateSync( + followTailLink: true, + visitLinks: true, + createChild: (DirectoryNode parent, bool isFinalSegment) { + if (recursive || isFinalSegment) { + return DirectoryNode(parent); + } + return null; + }, + ); + if (node?.type != expectedType) { + // There was an existing non-directory node at this object's path + throw common.notADirectory(path); + } + } + + @override + Future createTemp([String? prefix]) async => + createTempSync(prefix); + + @override + Directory createTempSync([String? prefix]) { + prefix = '${prefix ?? ''}rand'; + String fullPath = fileSystem.path.join(path, prefix); + String dirname = fileSystem.path.dirname(fullPath); + String basename = fileSystem.path.basename(fullPath); + DirectoryNode? node = fileSystem.findNode(dirname) as DirectoryNode?; + checkExists(node, () => dirname); + utils.checkIsDir(node!, () => dirname); + int tempCounter = _systemTempCounter[fileSystem] ?? 0; + String name() => '$basename$tempCounter'; + while (node.children.containsKey(name())) { + tempCounter++; + } + _systemTempCounter[fileSystem] = tempCounter; + DirectoryNode tempDir = DirectoryNode(node); + node.children[name()] = tempDir; + return MemoryDirectory(fileSystem, fileSystem.path.join(dirname, name())) + ..createSync(); + } + + @override + Future rename(String newPath) async => renameSync(newPath); + + @override + Directory renameSync(String newPath) => internalRenameSync( + newPath, + validateOverwriteExistingEntity: (DirectoryNode existingNode) { + if (existingNode.children.isNotEmpty) { + throw common.directoryNotEmpty(newPath); + } + }, + ) as Directory; + + @override + Directory get parent => + (backingOrNull?.isRoot ?? false) ? this : super.parent; + + @override + Directory get absolute => super.absolute as Directory; + + @override + Stream list({ + bool recursive = false, + bool followLinks = true, + }) => + Stream.fromIterable(listSync( + recursive: recursive, + followLinks: followLinks, + )); + + @override + List listSync({ + bool recursive = false, + bool followLinks = true, + }) { + DirectoryNode node = backing as DirectoryNode; + List listing = []; + List<_PendingListTask> tasks = <_PendingListTask>[ + _PendingListTask( + node, + path.endsWith(fileSystem.path.separator) + ? path.substring(0, path.length - 1) + : path, + {}, + ), + ]; + while (tasks.isNotEmpty) { + _PendingListTask task = tasks.removeLast(); + task.dir.children.forEach((String name, Node child) { + Set breadcrumbs = Set.from(task.breadcrumbs); + String childPath = fileSystem.path.join(task.path, name); + while (followLinks && + utils.isLink(child) && + breadcrumbs.add(child as LinkNode)) { + Node? referent = child.referentOrNull; + if (referent != null) { + child = referent; + } + } + if (utils.isDirectory(child)) { + listing.add(MemoryDirectory(fileSystem, childPath)); + if (recursive) { + tasks.add(_PendingListTask( + child as DirectoryNode, childPath, breadcrumbs)); + } + } else if (utils.isLink(child)) { + listing.add(MemoryLink(fileSystem, childPath)); + } else if (utils.isFile(child)) { + listing.add(MemoryFile(fileSystem, childPath)); + } + }); + } + return listing; + } + + @override + @protected + Directory clone(String path) => MemoryDirectory(fileSystem, path); + + @override + String toString() => "MemoryDirectory: '$path'"; +} + +class _PendingListTask { + _PendingListTask(this.dir, this.path, this.breadcrumbs); + final DirectoryNode dir; + final String path; + final Set breadcrumbs; +} diff --git a/pkgs/file/lib/src/backends/memory/memory_file.dart b/pkgs/file/lib/src/backends/memory/memory_file.dart new file mode 100644 index 000000000..ba4faab37 --- /dev/null +++ b/pkgs/file/lib/src/backends/memory/memory_file.dart @@ -0,0 +1,470 @@ +// Copyright (c) 2017, 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:async'; +import 'dart:convert'; +import 'dart:math' as math show min; +import 'dart:typed_data'; + +import 'package:file/file.dart'; +import 'package:file/src/backends/memory/operations.dart'; +import 'package:file/src/common.dart' as common; +import 'package:file/src/io.dart' as io; +import 'package:meta/meta.dart'; + +import 'common.dart'; +import 'memory_file_system_entity.dart'; +import 'memory_random_access_file.dart'; +import 'node.dart'; +import 'utils.dart' as utils; + +/// Internal implementation of [File]. +class MemoryFile extends MemoryFileSystemEntity implements File { + /// Instantiates a new [MemoryFile]. + const MemoryFile(NodeBasedFileSystem fileSystem, String path) + : super(fileSystem, path); + + FileNode get _resolvedBackingOrCreate { + Node? node = backingOrNull; + if (node == null) { + node = _doCreate(); + } else { + node = utils.isLink(node) + ? utils.resolveLinks(node as LinkNode, () => path) + : node; + utils.checkType(expectedType, node.type, () => path); + } + return node as FileNode; + } + + @override + io.FileSystemEntityType get expectedType => io.FileSystemEntityType.file; + + @override + bool existsSync() { + fileSystem.opHandle.call(path, FileSystemOp.exists); + return backingOrNull?.stat.type == expectedType; + } + + @override + Future create({bool recursive = false, bool exclusive = false}) async { + createSync(recursive: recursive, exclusive: exclusive); + return this; + } + + // TODO(dartbug.com/49647): Pass `exclusive` through after it lands. + @override + void createSync({bool recursive = false, bool exclusive = false}) { + fileSystem.opHandle(path, FileSystemOp.create); + _doCreate(recursive: recursive /*, exclusive: exclusive*/); + } + + Node? _doCreate({bool recursive = false}) { + Node? node = internalCreateSync( + followTailLink: true, + createChild: (DirectoryNode parent, bool isFinalSegment) { + if (isFinalSegment) { + return FileNode(parent); + } else if (recursive) { + return DirectoryNode(parent); + } + return null; + }, + ); + if (node?.type != expectedType) { + // There was an existing non-file entity at this object's path + assert(node?.type == FileSystemEntityType.directory); + throw common.isADirectory(path); + } + return node; + } + + @override + Future rename(String newPath) async => renameSync(newPath); + + @override + File renameSync(String newPath) => internalRenameSync( + newPath, + followTailLink: true, + checkType: (Node node) { + FileSystemEntityType actualType = node.stat.type; + if (actualType != expectedType) { + throw actualType == FileSystemEntityType.notFound + ? common.noSuchFileOrDirectory(path) + : common.isADirectory(path); + } + }, + ) as File; + + @override + Future copy(String newPath) async => copySync(newPath); + + @override + File copySync(String newPath) { + fileSystem.opHandle(path, FileSystemOp.copy); + FileNode sourceNode = resolvedBacking as FileNode; + fileSystem.findNode( + newPath, + segmentVisitor: ( + DirectoryNode parent, + String childName, + Node? child, + int currentSegment, + int finalSegment, + ) { + if (currentSegment == finalSegment) { + if (child != null) { + if (utils.isLink(child)) { + List ledger = []; + child = utils.resolveLinks(child as LinkNode, () => newPath, + ledger: ledger); + checkExists(child, () => newPath); + parent = child.parent; + childName = ledger.last; + assert(parent.children.containsKey(childName)); + } + utils.checkType(expectedType, child.type, () => newPath); + parent.children.remove(childName); + } + FileNode newNode = FileNode(parent); + newNode.copyFrom(sourceNode); + parent.children[childName] = newNode; + } + return child; + }, + ); + return clone(newPath); + } + + @override + Future length() async => lengthSync(); + + @override + int lengthSync() => (resolvedBacking as FileNode).size; + + @override + File get absolute => super.absolute as File; + + @override + Future lastAccessed() async => lastAccessedSync(); + + @override + DateTime lastAccessedSync() => (resolvedBacking as FileNode).stat.accessed; + + @override + Future setLastAccessed(DateTime time) async => + setLastAccessedSync(time); + + @override + void setLastAccessedSync(DateTime time) { + FileNode node = resolvedBacking as FileNode; + node.accessed = time.millisecondsSinceEpoch; + } + + @override + Future lastModified() async => lastModifiedSync(); + + @override + DateTime lastModifiedSync() => (resolvedBacking as FileNode).stat.modified; + + @override + Future setLastModified(DateTime time) async => + setLastModifiedSync(time); + + @override + void setLastModifiedSync(DateTime time) { + FileNode node = resolvedBacking as FileNode; + node.modified = time.millisecondsSinceEpoch; + } + + @override + Future open( + {io.FileMode mode = io.FileMode.read}) async => + openSync(mode: mode); + + @override + io.RandomAccessFile openSync({io.FileMode mode = io.FileMode.read}) { + fileSystem.opHandle(path, FileSystemOp.open); + if (utils.isWriteMode(mode) && !existsSync()) { + // [resolvedBacking] requires that the file already exists, so we must + // create it here first. + createSync(); + } + + return MemoryRandomAccessFile(path, resolvedBacking as FileNode, mode); + } + + @override + Stream> openRead([int? start, int? end]) { + fileSystem.opHandle(path, FileSystemOp.open); + try { + FileNode node = resolvedBacking as FileNode; + Uint8List content = node.content; + if (start != null) { + content = end == null + ? content.sublist(start) + : content.sublist(start, math.min(end, content.length)); + } + return Stream.value(content); + } catch (e) { + return Stream.error(e); + } + } + + @override + io.IOSink openWrite({ + io.FileMode mode = io.FileMode.write, + Encoding encoding = utf8, + }) { + fileSystem.opHandle(path, FileSystemOp.open); + if (!utils.isWriteMode(mode)) { + throw ArgumentError.value(mode, 'mode', + 'Must be either WRITE, APPEND, WRITE_ONLY, or WRITE_ONLY_APPEND'); + } + return _FileSink.fromFile(this, mode, encoding); + } + + @override + Future readAsBytes() async => readAsBytesSync(); + + @override + Uint8List readAsBytesSync() { + fileSystem.opHandle(path, FileSystemOp.read); + return Uint8List.fromList((resolvedBacking as FileNode).content); + } + + @override + Future readAsString({Encoding encoding = utf8}) async => + readAsStringSync(encoding: encoding); + + @override + String readAsStringSync({Encoding encoding = utf8}) { + try { + return encoding.decode(readAsBytesSync()); + } on FormatException catch (err) { + throw FileSystemException(err.message, path); + } + } + + @override + Future> readAsLines({Encoding encoding = utf8}) async => + readAsLinesSync(encoding: encoding); + + @override + List readAsLinesSync({Encoding encoding = utf8}) { + String str = readAsStringSync(encoding: encoding); + + if (str.isEmpty) { + return []; + } + + final List lines = str.split('\n'); + if (str.endsWith('\n')) { + // A final newline should not create an additional line. + lines.removeLast(); + } + + return lines; + } + + @override + Future writeAsBytes( + List bytes, { + io.FileMode mode = io.FileMode.write, + bool flush = false, + }) async { + writeAsBytesSync(bytes, mode: mode, flush: flush); + return this; + } + + @override + void writeAsBytesSync( + List bytes, { + io.FileMode mode = io.FileMode.write, + bool flush = false, + }) { + if (!utils.isWriteMode(mode)) { + throw common.badFileDescriptor(path); + } + FileNode node = _resolvedBackingOrCreate; + _truncateIfNecessary(node, mode); + fileSystem.opHandle(path, FileSystemOp.write); + node.write(bytes); + node.touch(); + } + + @override + Future writeAsString( + String contents, { + io.FileMode mode = io.FileMode.write, + Encoding encoding = utf8, + bool flush = false, + }) async { + writeAsStringSync(contents, mode: mode, encoding: encoding, flush: flush); + return this; + } + + @override + void writeAsStringSync( + String contents, { + io.FileMode mode = io.FileMode.write, + Encoding encoding = utf8, + bool flush = false, + }) => + writeAsBytesSync(encoding.encode(contents), mode: mode, flush: flush); + + @override + @protected + File clone(String path) => MemoryFile(fileSystem, path); + + void _truncateIfNecessary(FileNode node, io.FileMode mode) { + if (mode == io.FileMode.write || mode == io.FileMode.writeOnly) { + node.clear(); + } + } + + @override + String toString() => "MemoryFile: '$path'"; +} + +/// Implementation of an [io.IOSink] that's backed by a [FileNode]. +class _FileSink implements io.IOSink { + factory _FileSink.fromFile( + MemoryFile file, + io.FileMode mode, + Encoding encoding, + ) { + late FileNode node; + Exception? deferredException; + + // Resolve the backing immediately to ensure that the [FileNode] we write + // to is the same as when [openWrite] was called. This can matter if the + // file is moved or removed while open. + try { + node = file._resolvedBackingOrCreate; + } on Exception catch (e) { + // For behavioral consistency with [LocalFile], do not report failures + // immediately. + deferredException = e; + } + + Future future = Future.microtask(() { + if (deferredException != null) { + throw deferredException; + } + file._truncateIfNecessary(node, mode); + return node; + }); + return _FileSink._(future, encoding); + } + + _FileSink._(Future node, this.encoding) : _pendingWrites = node; + + final Completer _completer = Completer(); + + Future _pendingWrites; + Completer? _streamCompleter; + bool _isClosed = false; + + @override + Encoding encoding; + + bool get isStreaming => !(_streamCompleter?.isCompleted ?? true); + + @override + void add(List data) { + _checkNotStreaming(); + if (_isClosed) { + throw StateError('StreamSink is closed'); + } + + _addData(data); + } + + @override + void write(Object? obj) => add(encoding.encode(obj?.toString() ?? 'null')); + + @override + void writeAll(Iterable objects, [String separator = '']) { + bool firstIter = true; + for (dynamic obj in objects) { + if (!firstIter) { + write(separator); + } + firstIter = false; + write(obj); + } + } + + @override + void writeln([Object? obj = '']) { + write(obj); + write('\n'); + } + + @override + void writeCharCode(int charCode) => write(String.fromCharCode(charCode)); + + @override + void addError(Object error, [StackTrace? stackTrace]) { + _checkNotStreaming(); + _completer.completeError(error, stackTrace); + } + + @override + Future addStream(Stream> stream) { + _checkNotStreaming(); + _streamCompleter = Completer(); + + stream.listen( + (List data) => _addData(data), + cancelOnError: true, + onError: (Object error, StackTrace stackTrace) { + _streamCompleter!.completeError(error, stackTrace); + _streamCompleter = null; + }, + onDone: () { + _streamCompleter!.complete(); + _streamCompleter = null; + }, + ); + return _streamCompleter!.future; + } + + @override + Future flush() { + _checkNotStreaming(); + return _pendingWrites; + } + + @override + Future close() { + _checkNotStreaming(); + if (!_isClosed) { + _isClosed = true; + _pendingWrites.then( + (_) => _completer.complete(), + onError: (Object error, StackTrace stackTrace) => + _completer.completeError(error, stackTrace), + ); + } + return _completer.future; + } + + @override + Future get done => _completer.future; + + void _addData(List data) { + _pendingWrites = _pendingWrites.then((FileNode node) { + node.write(data); + return node; + }); + } + + void _checkNotStreaming() { + if (isStreaming) { + throw StateError('StreamSink is bound to a stream'); + } + } +} diff --git a/pkgs/file/lib/src/backends/memory/memory_file_stat.dart b/pkgs/file/lib/src/backends/memory/memory_file_stat.dart new file mode 100644 index 000000000..94f86d155 --- /dev/null +++ b/pkgs/file/lib/src/backends/memory/memory_file_stat.dart @@ -0,0 +1,68 @@ +// Copyright (c) 2017, 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:file/src/io.dart' as io; + +/// Internal implementation of [io.FileStat]. +class MemoryFileStat implements io.FileStat { + /// Creates a new [MemoryFileStat] with the specified properties. + const MemoryFileStat( + this.changed, + this.modified, + this.accessed, + this.type, + this.mode, + this.size, + ); + + MemoryFileStat._internalNotFound() + : changed = DateTime(0), + modified = DateTime(0), + accessed = DateTime(0), + type = io.FileSystemEntityType.notFound, + mode = 0, + size = -1; + + /// Shared instance representing a non-existent entity. + static final MemoryFileStat notFound = MemoryFileStat._internalNotFound(); + + @override + final DateTime changed; + + @override + final DateTime modified; + + @override + final DateTime accessed; + + @override + final io.FileSystemEntityType type; + + @override + final int mode; + + @override + final int size; + + @override + String modeString() { + int permissions = mode & 0xFFF; + List codes = const [ + '---', + '--x', + '-w-', + '-wx', + 'r--', + 'r-x', + 'rw-', + 'rwx', + ]; + List result = []; + result + ..add(codes[(permissions >> 6) & 0x7]) + ..add(codes[(permissions >> 3) & 0x7]) + ..add(codes[permissions & 0x7]); + return result.join(); + } +} diff --git a/pkgs/file/lib/src/backends/memory/memory_file_system.dart b/pkgs/file/lib/src/backends/memory/memory_file_system.dart new file mode 100644 index 000000000..f3cdaeede --- /dev/null +++ b/pkgs/file/lib/src/backends/memory/memory_file_system.dart @@ -0,0 +1,282 @@ +// Copyright (c) 2017, 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:file/file.dart'; +import 'package:file/src/backends/memory/operations.dart'; +import 'package:file/src/io.dart' as io; +import 'package:path/path.dart' as p; + +import 'clock.dart'; +import 'common.dart'; +import 'memory_directory.dart'; +import 'memory_file.dart'; +import 'memory_file_stat.dart'; +import 'memory_link.dart'; +import 'node.dart'; +import 'style.dart'; +import 'utils.dart' as utils; + +const String _thisDir = '.'; +const String _parentDir = '..'; + +void _defaultOpHandle(String context, FileSystemOp operation) {} + +/// An implementation of [FileSystem] that exists entirely in memory with an +/// internal representation loosely based on the Filesystem Hierarchy Standard. +/// +/// [MemoryFileSystem] is suitable for mocking and tests, as well as for +/// caching or staging before writing or reading to a live system. +/// +/// This implementation of the [FileSystem] interface does not directly use +/// any `dart:io` APIs; it merely uses the library's enum values and interfaces. +/// As such, it is suitable for use in the browser. +abstract class MemoryFileSystem implements StyleableFileSystem { + /// Creates a new `MemoryFileSystem`. + /// + /// The file system will be empty, and the current directory will be the + /// root directory. + /// + /// The clock will be a real-time clock; file modification times will + /// reflect the real time as reported by the operating system. + /// + /// If [style] is specified, the file system will use the specified path + /// style. The default is [FileSystemStyle.posix]. + factory MemoryFileSystem({ + FileSystemStyle style = FileSystemStyle.posix, + void Function(String context, FileSystemOp operation) opHandle = + _defaultOpHandle, + }) => + _MemoryFileSystem( + style: style, + clock: const Clock.realTime(), + opHandle: opHandle, + ); + + /// Creates a new `MemoryFileSystem` that has a fake clock. + /// + /// The file system will be empty, and the current directory will be the + /// root directory. + /// + /// The clock will increase monotonically each time it is used, disconnected + /// from any real-world clock. + /// + /// If [style] is specified, the file system will use the specified path + /// style. The default is [FileSystemStyle.posix]. + factory MemoryFileSystem.test({ + FileSystemStyle style = FileSystemStyle.posix, + void Function(String context, FileSystemOp operation) opHandle = + _defaultOpHandle, + }) => + _MemoryFileSystem( + style: style, + clock: Clock.monotonicTest(), + opHandle: opHandle, + ); +} + +/// Internal implementation of [MemoryFileSystem]. +class _MemoryFileSystem extends FileSystem + implements MemoryFileSystem, NodeBasedFileSystem { + _MemoryFileSystem({ + this.style = FileSystemStyle.posix, + required this.clock, + this.opHandle = _defaultOpHandle, + }) : _context = style.contextFor(style.root) { + _root = RootNode(this); + } + + RootNode? _root; + String? _systemTemp; + p.Context _context; + + @override + final Function(String context, FileSystemOp operation) opHandle; + + @override + final Clock clock; + + @override + final FileSystemStyle style; + + @override + RootNode? get root => _root; + + @override + String get cwd => _context.current; + + @override + Directory directory(dynamic path) => MemoryDirectory(this, getPath(path)); + + @override + File file(dynamic path) => MemoryFile(this, getPath(path)); + + @override + Link link(dynamic path) => MemoryLink(this, getPath(path)); + + @override + p.Context get path => _context; + + /// Gets the system temp directory. This directory will be created on-demand + /// in the root of the file system. Once created, its location is fixed for + /// the life of the process. + @override + Directory get systemTempDirectory { + _systemTemp ??= directory(style.root).createTempSync('.tmp_').path; + return directory(_systemTemp)..createSync(); + } + + @override + Directory get currentDirectory => directory(cwd); + + @override + set currentDirectory(dynamic path) { + String value; + if (path is io.Directory) { + value = path.path; + } else if (path is String) { + value = path; + } else { + throw ArgumentError('Invalid type for "path": ${path?.runtimeType}'); + } + + value = directory(value).resolveSymbolicLinksSync(); + Node? node = findNode(value); + checkExists(node, () => value); + utils.checkIsDir(node!, () => value); + assert(_context.isAbsolute(value)); + _context = style.contextFor(value); + } + + @override + Future stat(String path) async => statSync(path); + + @override + io.FileStat statSync(String path) { + try { + return findNode(path)?.stat ?? MemoryFileStat.notFound; + } on io.FileSystemException { + return MemoryFileStat.notFound; + } + } + + @override + Future identical(String path1, String path2) async => + identicalSync(path1, path2); + + @override + bool identicalSync(String path1, String path2) { + Node? node1 = findNode(path1); + checkExists(node1, () => path1); + Node? node2 = findNode(path2); + checkExists(node2, () => path2); + return node1 != null && node1 == node2; + } + + @override + bool get isWatchSupported => false; + + @override + Future type( + String path, { + bool followLinks = true, + }) async => + typeSync(path, followLinks: followLinks); + + @override + io.FileSystemEntityType typeSync(String path, {bool followLinks = true}) { + Node? node; + try { + node = findNode(path, followTailLink: followLinks); + } on io.FileSystemException { + node = null; + } + if (node == null) { + return io.FileSystemEntityType.notFound; + } + return node.type; + } + + /// Gets the node backing for the current working directory. Note that this + /// can return null if the directory has been deleted or moved from under our + /// feet. + DirectoryNode? get _current => findNode(cwd) as DirectoryNode?; + + @override + Node? findNode( + String path, { + Node? reference, + SegmentVisitor? segmentVisitor, + bool visitLinks = false, + List? pathWithSymlinks, + bool followTailLink = false, + }) { + if (path.isEmpty) { + return null; + } else if (_context.isAbsolute(path)) { + reference = _root; + path = path.substring(style.drive.length); + } else { + reference ??= _current; + } + + List parts = path.split(style.separator) + ..removeWhere(utils.isEmpty); + DirectoryNode? directory = reference?.directory; + Node? child = directory; + + int finalSegment = parts.length - 1; + for (int i = 0; i <= finalSegment; i++) { + String basename = parts[i]; + assert(basename.isNotEmpty); + + switch (basename) { + case _thisDir: + child = directory; + break; + case _parentDir: + child = directory?.parent; + directory = directory?.parent; + break; + default: + child = directory?.children[basename]; + } + + if (pathWithSymlinks != null) { + pathWithSymlinks.add(basename); + } + + // Generates a subpath for the current segment. + String subpath() => parts.sublist(0, i + 1).join(_context.separator); + + if (utils.isLink(child) && (i < finalSegment || followTailLink)) { + if (visitLinks || segmentVisitor == null) { + if (segmentVisitor != null) { + child = + segmentVisitor(directory!, basename, child, i, finalSegment); + } + child = utils.resolveLinks(child as LinkNode, subpath, + ledger: pathWithSymlinks); + } else { + child = utils.resolveLinks( + child as LinkNode, + subpath, + ledger: pathWithSymlinks, + tailVisitor: (DirectoryNode parent, String childName, Node? child) { + return segmentVisitor(parent, childName, child, i, finalSegment); + }, + ); + } + } else if (segmentVisitor != null) { + child = segmentVisitor(directory!, basename, child, i, finalSegment); + } + + if (i < finalSegment) { + checkExists(child, subpath); + utils.checkIsDir(child!, subpath); + directory = child as DirectoryNode; + } + } + return child; + } +} diff --git a/pkgs/file/lib/src/backends/memory/memory_file_system_entity.dart b/pkgs/file/lib/src/backends/memory/memory_file_system_entity.dart new file mode 100644 index 000000000..ad987d71a --- /dev/null +++ b/pkgs/file/lib/src/backends/memory/memory_file_system_entity.dart @@ -0,0 +1,309 @@ +// Copyright (c) 2017, 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:file/file.dart'; +import 'package:file/src/common.dart' as common; +import 'package:file/src/io.dart' as io; +import 'package:meta/meta.dart'; + +import 'common.dart'; +import 'memory_directory.dart'; +import 'node.dart'; +import 'operations.dart'; +import 'style.dart'; +import 'utils.dart' as utils; + +/// Validator function for use with `_renameSync`. This will be invoked if the +/// rename would overwrite an existing entity at the new path. If this operation +/// should not be allowed, this function is expected to throw a +/// [io.FileSystemException]. The lack of such an exception will be interpreted +/// as the overwrite being permissible. +typedef RenameOverwriteValidator = void Function( + T existingNode); + +/// Base class for all in-memory file system entity types. +abstract class MemoryFileSystemEntity implements FileSystemEntity { + /// Constructor for subclasses. + const MemoryFileSystemEntity(this.fileSystem, this.path); + + @override + final NodeBasedFileSystem fileSystem; + + @override + final String path; + + @override + String get dirname => fileSystem.path.dirname(path); + + @override + String get basename => fileSystem.path.basename(path); + + /// Returns the expected type of this entity, which may differ from the type + /// of the node that's found at the path specified by this entity. + io.FileSystemEntityType get expectedType; + + /// Gets the node that backs this file system entity, or null if this + /// entity does not exist. + @protected + Node? get backingOrNull { + try { + return fileSystem.findNode(path); + } on io.FileSystemException { + return null; + } + } + + /// Gets the node that backs this file system entity. Throws a + /// [io.FileSystemException] if this entity doesn't exist. + /// + /// The type of the node is not guaranteed to match [expectedType]. + @protected + Node get backing { + Node? node = fileSystem.findNode(path); + checkExists(node, () => path); + return node!; + } + + /// Gets the node that backs this file system entity, or if that node is + /// a symbolic link, the target node. This also will check that the type of + /// the node (after symlink resolution) matches [expectedType]. If the type + /// doesn't match, this will throw a [io.FileSystemException]. + @protected + Node get resolvedBacking { + Node node = backing; + node = utils.isLink(node) + ? utils.resolveLinks(node as LinkNode, () => path) + : node; + utils.checkType(expectedType, node.type, () => path); + return node; + } + + /// Checks the expected type of this file system entity against the specified + /// node's `stat` type, throwing a [FileSystemException] if the types don't + /// match. Note that since this checks the node's `stat` type, symbolic links + /// will be resolved to their target type for the purpose of this validation. + /// + /// Protected methods that accept a `checkType` argument will default to this + /// method if the `checkType` argument is unspecified. + @protected + void defaultCheckType(Node node) { + utils.checkType(expectedType, node.stat.type, () => path); + } + + @override + Uri get uri { + return Uri.file(path, windows: fileSystem.style == FileSystemStyle.windows); + } + + @override + Future exists() async => existsSync(); + + @override + Future resolveSymbolicLinks() async => resolveSymbolicLinksSync(); + + @override + String resolveSymbolicLinksSync() { + if (path.isEmpty) { + throw common.noSuchFileOrDirectory(path); + } + List ledger = []; + if (isAbsolute) { + ledger.add(fileSystem.style.drive); + } + Node? node = fileSystem.findNode(path, + pathWithSymlinks: ledger, followTailLink: true); + checkExists(node, () => path); + String resolved = ledger.join(fileSystem.path.separator); + if (resolved == fileSystem.style.drive) { + resolved = fileSystem.style.root; + } else if (!fileSystem.path.isAbsolute(resolved)) { + resolved = fileSystem.cwd + fileSystem.path.separator + resolved; + } + return fileSystem.path.normalize(resolved); + } + + @override + Future stat() => fileSystem.stat(path); + + @override + io.FileStat statSync() => fileSystem.statSync(path); + + @override + Future delete({bool recursive = false}) async { + deleteSync(recursive: recursive); + return this; + } + + @override + void deleteSync({bool recursive = false}) => + internalDeleteSync(recursive: recursive); + + @override + Stream watch({ + int events = io.FileSystemEvent.all, + bool recursive = false, + }) => + throw UnsupportedError('Watching not supported in MemoryFileSystem'); + + @override + bool get isAbsolute => fileSystem.path.isAbsolute(path); + + @override + FileSystemEntity get absolute { + String absolutePath = path; + if (!fileSystem.path.isAbsolute(absolutePath)) { + absolutePath = fileSystem.path.join(fileSystem.cwd, absolutePath); + } + return clone(absolutePath); + } + + @override + Directory get parent => MemoryDirectory(fileSystem, dirname); + + /// Helper method for subclasses wishing to synchronously create this entity. + /// This method will traverse the path to this entity one segment at a time, + /// calling [createChild] for each segment whose child does not already exist. + /// + /// When [createChild] is invoked: + /// - `parent` will be the parent node for the current segment and is + /// guaranteed to be non-null. + /// - `isFinalSegment` will indicate whether the current segment is the tail + /// segment, which in turn indicates that this is the segment into which to + /// create the node for this entity. + /// + /// This method returns with the backing node for the entity at this [path]. + /// If an entity already existed at this path, [createChild] will not be + /// invoked at all, and this method will return with the backing node for the + /// existing entity (whose type may differ from this entity's type). + /// + /// If [followTailLink] is true and the result node is a link, this will + /// resolve it to its target prior to returning it. + @protected + Node? internalCreateSync({ + required Node? Function(DirectoryNode parent, bool isFinalSegment) + createChild, + bool followTailLink = false, + bool visitLinks = false, + }) { + return fileSystem.findNode( + path, + followTailLink: followTailLink, + visitLinks: visitLinks, + segmentVisitor: ( + DirectoryNode parent, + String childName, + Node? child, + int currentSegment, + int finalSegment, + ) { + if (child == null) { + assert(!parent.children.containsKey(childName)); + child = createChild(parent, currentSegment == finalSegment); + if (child != null) { + parent.children[childName] = child; + } + } + return child; + }, + ); + } + + /// Helper method for subclasses wishing to synchronously rename this entity. + /// This method will look for an existing file system entity at the location + /// identified by [newPath], and if it finds an existing entity, it will check + /// the following: + /// + /// - If the entity is of a different type than this entity, the operation + /// will fail, and a [io.FileSystemException] will be thrown. + /// - If the caller has specified [validateOverwriteExistingEntity], then that + /// method will be invoked and passed the node backing of the existing + /// entity that would overwritten by the rename action. That callback is + /// expected to throw a [io.FileSystemException] if overwriting the existing + /// entity is not allowed. + /// + /// If the previous two checks pass, or if there was no existing entity at + /// the specified location, this will perform the rename. + /// + /// If [newPath] cannot be traversed to because its directory does not exist, + /// a [io.FileSystemException] will be thrown. + /// + /// If [followTailLink] is true and there is an existing link at the location + /// identified by [newPath], this will resolve the link to its target prior + /// to running the validation checks above. + /// + /// If [checkType] is specified, it will be used to validate that the file + /// system entity that exists at [path] is of the expected type. By default, + /// [defaultCheckType] is used to perform this validation. + @protected + FileSystemEntity internalRenameSync( + String newPath, { + RenameOverwriteValidator? validateOverwriteExistingEntity, + bool followTailLink = false, + utils.TypeChecker? checkType, + }) { + Node node = backing; + (checkType ?? defaultCheckType)(node); + fileSystem.findNode( + newPath, + segmentVisitor: ( + DirectoryNode parent, + String childName, + Node? child, + int currentSegment, + int finalSegment, + ) { + if (currentSegment == finalSegment) { + if (child != null) { + if (followTailLink) { + FileSystemEntityType childType = child.stat.type; + if (childType != FileSystemEntityType.notFound) { + utils.checkType(expectedType, child.stat.type, () => newPath); + } + } else { + utils.checkType(expectedType, child.type, () => newPath); + } + if (validateOverwriteExistingEntity != null) { + validateOverwriteExistingEntity(child as T); + } + parent.children.remove(childName); + } + node.parent.children.remove(basename); + parent.children[childName] = node; + node.parent = parent; + } + return child; + }, + ); + return clone(newPath); + } + + /// Deletes this entity from the node tree. + /// + /// If [checkType] is specified, it will be used to validate that the file + /// system entity that exists at [path] is of the expected type. By default, + /// [defaultCheckType] is used to perform this validation. + @protected + void internalDeleteSync({ + bool recursive = false, + utils.TypeChecker? checkType, + }) { + fileSystem.opHandle(path, FileSystemOp.delete); + Node node = backing; + if (!recursive) { + if (node is DirectoryNode && node.children.isNotEmpty) { + throw common.directoryNotEmpty(path); + } + (checkType ?? defaultCheckType)(node); + } + // Once we remove this reference, the node and all its children will be + // garbage collected; we don't need to explicitly delete all children in + // the recursive:true case. + node.parent.children.remove(basename); + } + + /// Creates a new entity with the same type as this entity but with the + /// specified path. + @protected + FileSystemEntity clone(String path); +} diff --git a/pkgs/file/lib/src/backends/memory/memory_link.dart b/pkgs/file/lib/src/backends/memory/memory_link.dart new file mode 100644 index 000000000..7d5afb42f --- /dev/null +++ b/pkgs/file/lib/src/backends/memory/memory_link.dart @@ -0,0 +1,113 @@ +// Copyright (c) 2017, 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:file/file.dart'; +import 'package:file/src/common.dart' as common; +import 'package:file/src/io.dart' as io; +import 'package:meta/meta.dart'; + +import 'memory_file_system_entity.dart'; +import 'node.dart'; +import 'operations.dart'; +import 'utils.dart' as utils; + +/// Internal implementation of [Link]. +class MemoryLink extends MemoryFileSystemEntity implements Link { + /// Instantiates a new [MemoryLink]. + const MemoryLink(NodeBasedFileSystem fileSystem, String path) + : super(fileSystem, path); + + @override + io.FileSystemEntityType get expectedType => io.FileSystemEntityType.link; + + @override + bool existsSync() { + fileSystem.opHandle.call(path, FileSystemOp.exists); + return backingOrNull?.type == expectedType; + } + + @override + Future rename(String newPath) async => renameSync(newPath); + + @override + Link renameSync(String newPath) => internalRenameSync( + newPath, + checkType: (Node node) { + if (node.type != expectedType) { + throw node.type == FileSystemEntityType.directory + ? common.isADirectory(newPath) + : common.invalidArgument(newPath); + } + }, + ) as Link; + + @override + Future create(String target, {bool recursive = false}) async { + createSync(target, recursive: recursive); + return this; + } + + @override + void createSync(String target, {bool recursive = false}) { + bool preexisting = true; + fileSystem.opHandle(path, FileSystemOp.create); + internalCreateSync( + createChild: (DirectoryNode parent, bool isFinalSegment) { + if (isFinalSegment) { + preexisting = false; + return LinkNode(parent, target); + } else if (recursive) { + return DirectoryNode(parent); + } + return null; + }); + if (preexisting) { + // Per the spec, this is an error. + throw common.fileExists(path); + } + } + + @override + Future update(String target) async { + updateSync(target); + return this; + } + + @override + void updateSync(String target) { + Node node = backing; + utils.checkType(expectedType, node.type, () => path); + (node as LinkNode).target = target; + } + + @override + void deleteSync({bool recursive = false}) => internalDeleteSync( + recursive: recursive, + checkType: (Node node) => + utils.checkType(expectedType, node.type, () => path), + ); + + @override + Future target() async => targetSync(); + + @override + String targetSync() { + Node node = backing; + if (node.type != expectedType) { + // Note: this may change; https://github.com/dart-lang/sdk/issues/28204 + throw common.noSuchFileOrDirectory(path); + } + return (node as LinkNode).target; + } + + @override + Link get absolute => super.absolute as Link; + + @override + @protected + Link clone(String path) => MemoryLink(fileSystem, path); + + @override + String toString() => "MemoryLink: '$path'"; +} diff --git a/pkgs/file/lib/src/backends/memory/memory_random_access_file.dart b/pkgs/file/lib/src/backends/memory/memory_random_access_file.dart new file mode 100644 index 000000000..d4fe73d18 --- /dev/null +++ b/pkgs/file/lib/src/backends/memory/memory_random_access_file.dart @@ -0,0 +1,390 @@ +// Copyright (c) 2020, 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:convert'; +import 'dart:math' as math show min; +import 'dart:typed_data'; + +import 'package:file/src/common.dart' as common; +import 'package:file/src/io.dart' as io; + +import 'memory_file.dart'; +import 'node.dart'; +import 'utils.dart' as utils; + +/// A [MemoryFileSystem]-backed implementation of [io.RandomAccessFile]. +class MemoryRandomAccessFile implements io.RandomAccessFile { + /// Constructs a [MemoryRandomAccessFile]. + /// + /// This should be used only by [MemoryFile.open] or [MemoryFile.openSync]. + MemoryRandomAccessFile(this.path, this._node, this._mode) { + switch (_mode) { + case io.FileMode.read: + break; + case io.FileMode.write: + case io.FileMode.writeOnly: + truncateSync(0); + break; + case io.FileMode.append: + case io.FileMode.writeOnlyAppend: + _position = lengthSync(); + break; + default: + // [FileMode] provides no way of retrieving its value or name. + throw UnimplementedError('Unsupported FileMode'); + } + } + + @override + final String path; + + final FileNode _node; + final io.FileMode _mode; + + bool _isOpen = true; + int _position = 0; + + /// Whether an asynchronous operation is pending. + /// + /// See [_asyncWrapper] for details. + bool get _asyncOperationPending => __asyncOperationPending; + + set _asyncOperationPending(bool value) { + assert(__asyncOperationPending != value); + __asyncOperationPending = value; + } + + bool __asyncOperationPending = false; + + /// Throws a [io.FileSystemException] if an operation is attempted on a file + /// that is not open. + void _checkOpen() { + if (!_isOpen) { + throw io.FileSystemException('File closed', path); + } + } + + /// Throws a [io.FileSystemException] if attempting to read from a file that + /// has not been opened for reading. + void _checkReadable(String operation) { + switch (_mode) { + case io.FileMode.read: + case io.FileMode.write: + case io.FileMode.append: + return; + case io.FileMode.writeOnly: + case io.FileMode.writeOnlyAppend: + default: + throw io.FileSystemException( + '$operation failed', path, common.badFileDescriptor(path).osError); + } + } + + /// Throws a [io.FileSystemException] if attempting to read from a file that + /// has not been opened for writing. + void _checkWritable(String operation) { + if (utils.isWriteMode(_mode)) { + return; + } + + throw io.FileSystemException( + '$operation failed', path, common.badFileDescriptor(path).osError); + } + + /// Throws a [io.FileSystemException] if attempting to perform an operation + /// while an asynchronous operation is already in progress. + /// + /// See [_asyncWrapper] for details. + void _checkAsync() { + if (_asyncOperationPending) { + throw io.FileSystemException( + 'An async operation is currently pending', path); + } + } + + /// Wraps a synchronous function to make it appear asynchronous. + /// + /// [_asyncOperationPending], [_checkAsync], and [_asyncWrapper] are used to + /// mimic [RandomAccessFile]'s enforcement that only one asynchronous + /// operation is pending for a [RandomAccessFile] instance. Since + /// [MemoryFileSystem]-based classes are likely to be used in tests, fidelity + /// is important to catch errors that might occur in production. + /// + /// [_asyncWrapper] does not call [f] directly since setting and unsetting + /// [_asyncOperationPending] synchronously would not be meaningful. We + /// instead execute [f] through a [Future.delayed] callback to better simulate + /// asynchrony. + Future _asyncWrapper(R Function() f) async { + _checkAsync(); + + _asyncOperationPending = true; + try { + return await Future.delayed( + Duration.zero, + () { + // Temporarily reset [_asyncOpPending] in case [f]'s has its own + // checks for pending asynchronous operations. + _asyncOperationPending = false; + try { + return f(); + } finally { + _asyncOperationPending = true; + } + }, + ); + } finally { + _asyncOperationPending = false; + } + } + + @override + Future close() async => _asyncWrapper(closeSync); + + @override + void closeSync() { + _checkOpen(); + _isOpen = false; + } + + @override + Future flush() async { + await _asyncWrapper(flushSync); + return this; + } + + @override + void flushSync() { + _checkOpen(); + _checkAsync(); + } + + @override + Future length() => _asyncWrapper(lengthSync); + + @override + int lengthSync() { + _checkOpen(); + _checkAsync(); + return _node.size; + } + + @override + Future lock([ + io.FileLock mode = io.FileLock.exclusive, + int start = 0, + int end = -1, + ]) async { + await _asyncWrapper(() => lockSync(mode, start, end)); + return this; + } + + @override + void lockSync([ + io.FileLock mode = io.FileLock.exclusive, + int start = 0, + int end = -1, + ]) { + _checkOpen(); + _checkAsync(); + // TODO(jamesderlin): Implement, https://github.com/google/file.dart/issues/140 + throw UnimplementedError('TODO'); + } + + @override + Future position() => _asyncWrapper(positionSync); + + @override + int positionSync() { + _checkOpen(); + _checkAsync(); + return _position; + } + + @override + Future read(int bytes) => _asyncWrapper(() => readSync(bytes)); + + @override + Uint8List readSync(int bytes) { + _checkOpen(); + _checkAsync(); + _checkReadable('read'); + // TODO(jamesderlin): Check for integer overflow. + final int end = math.min(_position + bytes, lengthSync()); + final Uint8List copy = _node.content.sublist(_position, end); + _position = end; + return copy; + } + + @override + Future readByte() => _asyncWrapper(readByteSync); + + @override + int readByteSync() { + _checkOpen(); + _checkAsync(); + _checkReadable('readByte'); + + if (_position >= lengthSync()) { + return -1; + } + return _node.content[_position++]; + } + + @override + Future readInto(List buffer, [int start = 0, int? end]) => + _asyncWrapper(() => readIntoSync(buffer, start, end)); + + @override + int readIntoSync(List buffer, [int start = 0, int? end]) { + _checkOpen(); + _checkAsync(); + _checkReadable('readInto'); + + end = RangeError.checkValidRange(start, end, buffer.length); + + final int length = lengthSync(); + int i; + for (i = start; i < end && _position < length; i += 1, _position += 1) { + buffer[i] = _node.content[_position]; + } + return i - start; + } + + @override + Future setPosition(int position) async { + await _asyncWrapper(() => setPositionSync(position)); + return this; + } + + @override + void setPositionSync(int position) { + _checkOpen(); + _checkAsync(); + + if (position < 0) { + throw io.FileSystemException( + 'setPosition failed', path, common.invalidArgument(path).osError); + } + + // Empirical testing indicates that setting the position to be beyond the + // end of the file is legal and will zero-fill upon the next write. + _position = position; + } + + @override + Future truncate(int length) async { + await _asyncWrapper(() => truncateSync(length)); + return this; + } + + @override + void truncateSync(int length) { + _checkOpen(); + _checkAsync(); + + if (length < 0 || !utils.isWriteMode(_mode)) { + throw io.FileSystemException( + 'truncate failed', path, common.invalidArgument(path).osError); + } + + final int oldLength = lengthSync(); + if (length < oldLength) { + _node.truncate(length); + + // [_position] is intentionally left untouched to match the observed + // behavior of [RandomAccessFile]. + } else if (length > oldLength) { + _node.write(Uint8List(length - oldLength)); + } + assert(lengthSync() == length); + } + + @override + Future unlock([int start = 0, int end = -1]) async { + await _asyncWrapper(() => unlockSync(start, end)); + return this; + } + + @override + void unlockSync([int start = 0, int end = -1]) { + _checkOpen(); + _checkAsync(); + // TODO(jamesderlin): Implement, https://github.com/google/file.dart/issues/140 + throw UnimplementedError('TODO'); + } + + @override + Future writeByte(int value) async { + await _asyncWrapper(() => writeByteSync(value)); + return this; + } + + @override + int writeByteSync(int value) { + _checkOpen(); + _checkAsync(); + _checkWritable('writeByte'); + + // [Uint8List] will truncate values to 8-bits automatically, so we don't + // need to check [value]. + + int length = lengthSync(); + if (_position >= length) { + // If [_position] is out of bounds, [RandomAccessFile] zero-fills the + // file. + truncateSync(_position + 1); + length = lengthSync(); + } + assert(_position < length); + _node.content[_position++] = value; + + // Despite what the documentation states, [RandomAccessFile.writeByteSync] + // always seems to return 1, even if we had to extend the file for an out of + // bounds write. See https://github.com/dart-lang/sdk/issues/42298. + return 1; + } + + @override + Future writeFrom( + List buffer, [ + int start = 0, + int? end, + ]) async { + await _asyncWrapper(() => writeFromSync(buffer, start, end)); + return this; + } + + @override + void writeFromSync(List buffer, [int start = 0, int? end]) { + _checkOpen(); + _checkAsync(); + _checkWritable('writeFrom'); + + end = RangeError.checkValidRange(start, end, buffer.length); + + final int writeByteCount = end - start; + final int endPosition = _position + writeByteCount; + + if (endPosition > lengthSync()) { + truncateSync(endPosition); + } + + _node.content.setRange(_position, endPosition, buffer, start); + _position = endPosition; + } + + @override + Future writeString( + String string, { + Encoding encoding = utf8, + }) async { + await _asyncWrapper(() => writeStringSync(string, encoding: encoding)); + return this; + } + + @override + void writeStringSync(String string, {Encoding encoding = utf8}) { + writeFromSync(encoding.encode(string)); + } +} diff --git a/pkgs/file/lib/src/backends/memory/node.dart b/pkgs/file/lib/src/backends/memory/node.dart new file mode 100644 index 000000000..ae4d3f75d --- /dev/null +++ b/pkgs/file/lib/src/backends/memory/node.dart @@ -0,0 +1,358 @@ +// Copyright (c) 2017, 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:typed_data'; + +import 'package:file/file.dart'; +import 'package:file/src/backends/memory/operations.dart'; +import 'package:file/src/io.dart' as io; + +import 'clock.dart'; +import 'common.dart'; +import 'memory_file_stat.dart'; +import 'style.dart'; + +/// Visitor callback for use with [NodeBasedFileSystem.findNode]. +/// +/// [parent] is the parent node of the current path segment and is guaranteed +/// to be non-null. +/// +/// [childName] is the basename of the entity at the current path segment. It +/// is guaranteed to be non-null. +/// +/// [childNode] is the node at the current path segment. It will be +/// non-null only if such an entity exists. The return value of this callback +/// will be used as the value of this node, which allows this callback to +/// do things like recursively create or delete folders. +/// +/// [currentSegment] is the index of the current segment within the overall +/// path that's being walked by [NodeBasedFileSystem.findNode]. +/// +/// [finalSegment] is the index of the final segment that will be walked by +/// [NodeBasedFileSystem.findNode]. +typedef SegmentVisitor = Node? Function( + DirectoryNode parent, + String childName, + Node? childNode, + int currentSegment, + int finalSegment, +); + +/// A [FileSystem] whose internal structure is made up of a tree of [Node] +/// instances, rooted at a single node. +abstract class NodeBasedFileSystem implements StyleableFileSystem { + /// An optional handle to hook into common file system operations. + void Function(String context, FileSystemOp operation) get opHandle; + + /// The root node. + RootNode? get root; + + /// The path of the current working directory. + String get cwd; + + /// The clock to use when finding the current time (e.g. to set the creation + /// time of a new node). + Clock get clock; + + /// Gets the backing node of the entity at the specified path. If the tail + /// element of the path does not exist, this will return null. If the tail + /// element cannot be reached because its directory does not exist, a + /// [io.FileSystemException] will be thrown. + /// + /// If [path] is a relative path, it will be resolved relative to + /// [reference], or the current working directory ([cwd]) if [reference] is + /// null. If [path] is an absolute path, [reference] will be ignored. + /// + /// If the last element in [path] represents a symbolic link, this will + /// return the [LinkNode] node for the link (it will not return the + /// node to which the link points), unless [followTailLink] is true. + /// Directory links in the _middle_ of the path will be followed in order to + /// find the node regardless of the value of [followTailLink]. + /// + /// If [segmentVisitor] is specified, it will be invoked for every path + /// segment visited along the way starting where the reference (root folder + /// if the path is absolute) is the parent. For each segment, the return value + /// of [segmentVisitor] will be used as the backing node of that path + /// segment, thus allowing callers to create nodes on demand in the + /// specified path. Note that `..` and `.` segments may cause the visitor to + /// get invoked with the same node multiple times. When [segmentVisitor] is + /// invoked, for each path segment that resolves to a link node, the visitor + /// will visit the actual link node if [visitLinks] is true; otherwise it + /// will visit the target of the link node. + /// + /// If [pathWithSymlinks] is specified, the path to the node with symbolic + /// links explicitly broken out will be appended to the buffer. `..` and `.` + /// path segments will *not* be resolved and are left to the caller. + Node? findNode( + String path, { + Node reference, + SegmentVisitor segmentVisitor, + bool visitLinks = false, + List pathWithSymlinks, + bool followTailLink = false, + }); +} + +/// A class that represents the actual storage of an existent file system +/// entity (whereas classes [File], [Directory], and [Link] represent less +/// concrete entities that may or may not yet exist). +/// +/// This data structure is loosely based on a Unix-style file system inode +/// (hence the name). +abstract class Node { + /// Constructs a new [Node] as a child of the specified parent. + Node(this._parent) { + if (_parent == null && !isRoot) { + throw const io.FileSystemException('All nodes must have a parent.'); + } + } + + DirectoryNode? _parent; + + /// Gets the directory that holds this node. + DirectoryNode get parent => _parent!; + + /// Reparents this node to live in the specified directory. + set parent(DirectoryNode parent) { + DirectoryNode ancestor = parent; + while (!ancestor.isRoot) { + if (ancestor == this) { + throw const io.FileSystemException( + 'A directory cannot be its own ancestor.'); + } + ancestor = ancestor.parent; + } + _parent = parent; + } + + /// Returns the type of the file system entity that this node represents. + io.FileSystemEntityType get type; + + /// Returns the POSIX stat information for this file system object. + io.FileStat get stat; + + /// Returns the closest directory in the ancestry hierarchy starting with + /// this node. For directory nodes, it returns the node itself; for other + /// nodes, it returns the parent node. + DirectoryNode get directory => _parent!; + + /// Tells if this node is a root node. + bool get isRoot => false; + + /// Returns the file system responsible for this node. + NodeBasedFileSystem get fs => _parent!.fs; +} + +/// Base class that represents the backing for those nodes that have +/// substance (namely, node types that will not redirect to other types when +/// you call [stat] on them). +abstract class RealNode extends Node { + /// Constructs a new [RealNode] as a child of the specified [parent]. + RealNode(DirectoryNode? parent) : super(parent) { + int now = clock.now.millisecondsSinceEpoch; + changed = now; + modified = now; + accessed = now; + } + + /// See [NodeBasedFileSystem.clock]. + Clock get clock => parent.clock; + + /// Last changed time in milliseconds since the Epoch. + late int changed; + + /// Last modified time in milliseconds since the Epoch. + late int modified; + + /// Last accessed time in milliseconds since the Epoch. + late int accessed; + + /// Bitmask representing the file read/write/execute mode. + int mode = 0x777; + + @override + io.FileStat get stat { + return MemoryFileStat( + DateTime.fromMillisecondsSinceEpoch(changed), + DateTime.fromMillisecondsSinceEpoch(modified), + DateTime.fromMillisecondsSinceEpoch(accessed), + type, + mode, + size, + ); + } + + /// The size of the file system entity in bytes. + int get size; + + /// Updates the last modified time of the node. + void touch() { + modified = clock.now.millisecondsSinceEpoch; + } +} + +/// Class that represents the backing for an in-memory directory. +class DirectoryNode extends RealNode { + /// Constructs a new [DirectoryNode] as a child of the specified [parent]. + DirectoryNode(DirectoryNode? parent) : super(parent); + + /// Child nodes, indexed by their basename. + final Map children = {}; + + @override + io.FileSystemEntityType get type => io.FileSystemEntityType.directory; + + @override + DirectoryNode get directory => this; + + @override + int get size => 0; +} + +/// Class that represents the backing for the root of the in-memory file system. +class RootNode extends DirectoryNode { + /// Constructs a new [RootNode] tied to the specified file system. + RootNode(this.fs) + : assert(fs.root == null), + super(null); + + @override + final NodeBasedFileSystem fs; + + @override + Clock get clock => fs.clock; + + @override + DirectoryNode get parent => this; + + @override + bool get isRoot => true; + + @override + set parent(DirectoryNode parent) => + throw UnsupportedError('Cannot set the parent of the root directory.'); +} + +/// Class that represents the backing for an in-memory regular file. +class FileNode extends RealNode { + /// Constructs a new [FileNode] as a child of the specified [parent]. + FileNode(DirectoryNode parent) : super(parent); + + /// File contents in bytes. + Uint8List get content => _content; + Uint8List _content = Uint8List(0); + + @override + io.FileSystemEntityType get type => io.FileSystemEntityType.file; + + @override + int get size => _content.length; + + /// Appends the specified bytes to the end of this node's [content]. + void write(List bytes) { + Uint8List existing = _content; + _content = Uint8List(existing.length + bytes.length); + _content.setRange(0, existing.length, existing); + _content.setRange(existing.length, _content.length, bytes); + } + + /// Truncates this node's [content] to the specified length. + /// + /// [length] must be in the range \[0, [size]\]. + void truncate(int length) { + assert(length >= 0); + assert(length <= _content.length); + _content = _content.sublist(0, length); + } + + /// Clears the [content] of the node. + void clear() { + _content = Uint8List(0); + } + + /// Copies data from [source] into this node. The [modified] and [changed] + /// fields will be reset as opposed to copied to indicate that this file + /// has been modified and changed. + void copyFrom(FileNode source) { + modified = changed = clock.now.millisecondsSinceEpoch; + accessed = source.accessed; + mode = source.mode; + _content = Uint8List.fromList(source.content); + } +} + +/// Class that represents the backing for an in-memory symbolic link. +class LinkNode extends Node { + /// Constructs a new [LinkNode] as a child of the specified [parent] and + /// linking to the specified [target] path. + LinkNode(DirectoryNode parent, this.target) + : assert(target.isNotEmpty), + super(parent); + + /// The path to which this link points. + String target; + + /// A marker used to detect circular link references. + bool _reentrant = false; + + /// Gets the node backing for this link's target. Throws a + /// [FileSystemException] if this link references a non-existent file + /// system entity. + /// + /// If [tailVisitor] is specified, it will be invoked for the tail path + /// segment of this link's target, and its return value will be used as the + /// return value of this method. If the tail path segment of this link's + /// target cannot be traversed into, a [FileSystemException] will be thrown, + /// and [tailVisitor] will not be invoked. + Node getReferent({ + Node? Function(DirectoryNode parent, String childName, Node? child)? + tailVisitor, + }) { + Node? referent = fs.findNode( + target, + reference: this, + segmentVisitor: ( + DirectoryNode parent, + String childName, + Node? child, + int currentSegment, + int finalSegment, + ) { + if (tailVisitor != null && currentSegment == finalSegment) { + child = tailVisitor(parent, childName, child); + } + return child; + }, + ); + checkExists(referent, () => target); + return referent!; + } + + /// Gets the node backing for this link's target, or null if this link + /// references a non-existent file system entity. + Node? get referentOrNull { + try { + return getReferent(); + } on io.FileSystemException { + return null; + } + } + + @override + io.FileSystemEntityType get type => io.FileSystemEntityType.link; + + @override + io.FileStat get stat { + if (_reentrant) { + return MemoryFileStat.notFound; + } + _reentrant = true; + try { + Node? node = referentOrNull; + return node == null ? MemoryFileStat.notFound : node.stat; + } finally { + _reentrant = false; + } + } +} diff --git a/pkgs/file/lib/src/backends/memory/operations.dart b/pkgs/file/lib/src/backends/memory/operations.dart new file mode 100644 index 000000000..9fc7462fc --- /dev/null +++ b/pkgs/file/lib/src/backends/memory/operations.dart @@ -0,0 +1,86 @@ +// Copyright (c) 2017, 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 file system operation used by the [MemoryFileSytem] to allow +/// tests to insert errors for certain operations. +/// +/// This is not implemented as an enum to allow new values to be added in a +/// backwards compatible manner. +class FileSystemOp { + const FileSystemOp._(this._value); + + // This field added to ensure const values can be different. + // ignore: unused_field + final int _value; + + /// A file system operation used for all read methods. + /// + /// * [FileSystemEntity.readAsString] + /// * [FileSystemEntity.readAsStringSync] + /// * [FileSystemEntity.readAsBytes] + /// * [FileSystemEntity.readAsBytesSync] + static const FileSystemOp read = FileSystemOp._(0); + + /// A file system operation used for all write methods. + /// + /// * [FileSystemEntity.writeAsString] + /// * [FileSystemEntity.writeAsStringSync] + /// * [FileSystemEntity.writeAsBytes] + /// * [FileSystemEntity.writeAsBytesSync] + static const FileSystemOp write = FileSystemOp._(1); + + /// A file system operation used for all delete methods. + /// + /// * [FileSystemEntity.delete] + /// * [FileSystemEntity.deleteSync] + static const FileSystemOp delete = FileSystemOp._(2); + + /// A file system operation used for all create methods. + /// + /// * [FileSystemEntity.create] + /// * [FileSystemEntity.createSync] + static const FileSystemOp create = FileSystemOp._(3); + + /// A file operation used for all open methods. + /// + /// * [File.open] + /// * [File.openSync] + /// * [File.openRead] + /// * [File.openWrite] + static const FileSystemOp open = FileSystemOp._(4); + + /// A file operation used for all copy methods. + /// + /// * [File.copy] + /// * [File.copySync] + static const FileSystemOp copy = FileSystemOp._(5); + + /// A file system operation used for all exists methods. + /// + /// * [FileSystemEntity.exists] + /// * [FileSystemEntity.existsSync] + static const FileSystemOp exists = FileSystemOp._(6); + + @override + String toString() { + switch (_value) { + case 0: + return 'FileSystemOp.read'; + case 1: + return 'FileSystemOp.write'; + case 2: + return 'FileSystemOp.delete'; + case 3: + return 'FileSystemOp.create'; + case 4: + return 'FileSystemOp.open'; + case 5: + return 'FileSystemOp.copy'; + case 6: + return 'FileSystemOp.exists'; + default: + throw StateError('Invalid FileSytemOp type: $this'); + } + } +} diff --git a/pkgs/file/lib/src/backends/memory/style.dart b/pkgs/file/lib/src/backends/memory/style.dart new file mode 100644 index 000000000..701c9d05e --- /dev/null +++ b/pkgs/file/lib/src/backends/memory/style.dart @@ -0,0 +1,98 @@ +// 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:file/file.dart'; +import 'package:path/path.dart' as p; + +/// Class that represents the path style that a memory file system should +/// adopt. +/// +/// This is primarily useful if you want to test how your code will behave +/// when faced with particular paths or particular path separator characters. +/// For instance, you may want to test that your code will work on Windows, +/// while still using a memory file system in order to gain hermeticity in your +/// tests. +abstract class FileSystemStyle { + const FileSystemStyle._(); + + /// Mimics the Unix file system style. + /// + /// * This style does not have the notion of drives + /// * All file system paths are rooted at `/` + /// * The path separator is `/` + /// + /// An example path in this style is `/path/to/file`. + static const FileSystemStyle posix = _Posix(); + + /// Mimics the Windows file system style. + /// + /// * This style mounts its root folder on a single root drive (`C:`) + /// * All file system paths are rooted at `C:\` + /// * The path separator is `\` + /// + /// An example path in this style is `C:\path\to\file`. + static const FileSystemStyle windows = _Windows(); + + /// The drive upon which the root directory is mounted. + /// + /// While real-world file systems that have the notion of drives will support + /// multiple drives per system, memory file system will only support one + /// root drive. + /// + /// This will be the empty string for styles that don't have the notion of + /// drives (e.g. [posix]). + String get drive; + + /// The String that represents the delineation between a directory and its + /// children. + String get separator; + + /// The string that represents the root of the file system. + /// + /// Memory file system is always single-rooted. + String get root => '$drive$separator'; + + /// Gets an object useful for manipulating paths in this style. + /// + /// Relative path manipulations will be relative to the specified [path]. + p.Context contextFor(String path); +} + +class _Posix extends FileSystemStyle { + const _Posix() : super._(); + + @override + String get drive => ''; + + @override + String get separator { + return p.Style.posix.separator; // ignore: deprecated_member_use + } + + @override + p.Context contextFor(String path) => + p.Context(style: p.Style.posix, current: path); +} + +class _Windows extends FileSystemStyle { + const _Windows() : super._(); + + @override + String get drive => 'C:'; + + @override + String get separator { + return p.Style.windows.separator; // ignore: deprecated_member_use + } + + @override + p.Context contextFor(String path) => + p.Context(style: p.Style.windows, current: path); +} + +/// A file system that supports different styles. +abstract class StyleableFileSystem implements FileSystem { + /// The style used by this file system. + FileSystemStyle get style; +} diff --git a/pkgs/file/lib/src/backends/memory/utils.dart b/pkgs/file/lib/src/backends/memory/utils.dart new file mode 100644 index 000000000..eec998038 --- /dev/null +++ b/pkgs/file/lib/src/backends/memory/utils.dart @@ -0,0 +1,117 @@ +// Copyright (c) 2017, 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:file/file.dart'; +import 'package:file/src/common.dart' as common; +import 'package:file/src/io.dart' as io; + +import 'common.dart'; +import 'node.dart'; + +/// Checks if `node.type` returns [io.FileSystemEntityType.FILE]. +bool isFile(Node? node) => node?.type == io.FileSystemEntityType.file; + +/// Checks if `node.type` returns [io.FileSystemEntityType.DIRECTORY]. +bool isDirectory(Node? node) => node?.type == io.FileSystemEntityType.directory; + +/// Checks if `node.type` returns [io.FileSystemEntityType.LINK]. +bool isLink(Node? node) => node?.type == io.FileSystemEntityType.link; + +/// Validator function that is expected to throw a [FileSystemException] if +/// the node does not represent the type that is expected in any given context. +typedef TypeChecker = void Function(Node node); + +/// Throws a [io.FileSystemException] if [node] is not a directory. +void checkIsDir(Node node, PathGenerator path) { + if (!isDirectory(node)) { + throw common.notADirectory(path() as String); + } +} + +/// Throws a [io.FileSystemException] if [expectedType] doesn't match +/// [actualType]. +void checkType( + FileSystemEntityType expectedType, + FileSystemEntityType actualType, + PathGenerator path, +) { + if (expectedType != actualType) { + switch (expectedType) { + case FileSystemEntityType.directory: + throw common.notADirectory(path() as String); + case FileSystemEntityType.file: + assert(actualType == FileSystemEntityType.directory); + throw common.isADirectory(path() as String); + case FileSystemEntityType.link: + throw common.invalidArgument(path() as String); + default: + // Should not happen + throw AssertionError(); + } + } +} + +/// Tells if the specified file mode represents a write mode. +bool isWriteMode(io.FileMode mode) => + mode == io.FileMode.write || + mode == io.FileMode.append || + mode == io.FileMode.writeOnly || + mode == io.FileMode.writeOnlyAppend; + +/// Tells whether the given string is empty. +bool isEmpty(String str) => str.isEmpty; + +/// Returns the node ultimately referred to by [link]. This will resolve +/// the link references (following chains of links as necessary) and return +/// the node at the end of the link chain. +/// +/// If a loop in the link chain is found, this will throw a +/// [FileSystemException], calling [path] to generate the path. +/// +/// If [ledger] is specified, the resolved path to the terminal node will be +/// appended to the ledger (or overwritten in the ledger if a link target +/// specified an absolute path). The path will not be normalized, meaning +/// `..` and `.` path segments may be present. +/// +/// If [tailVisitor] is specified, it will be invoked for the tail element of +/// the last link in the symbolic link chain, and its return value will be the +/// return value of this method (thus allowing callers to create the entity +/// at the end of the chain on demand). +Node resolveLinks( + LinkNode link, + PathGenerator path, { + List? ledger, + Node? Function(DirectoryNode parent, String childName, Node? child)? + tailVisitor, +}) { + // Record a breadcrumb trail to guard against symlink loops. + Set breadcrumbs = {}; + + Node node = link; + while (isLink(node)) { + link = node as LinkNode; + if (!breadcrumbs.add(link)) { + throw common.tooManyLevelsOfSymbolicLinks(path() as String); + } + if (ledger != null) { + if (link.fs.path.isAbsolute(link.target)) { + ledger.clear(); + } else if (ledger.isNotEmpty) { + ledger.removeLast(); + } + ledger.addAll(link.target.split(link.fs.path.separator)); + } + node = link.getReferent( + tailVisitor: (DirectoryNode parent, String childName, Node? child) { + if (tailVisitor != null && !isLink(child)) { + // Only invoke [tailListener] on the final resolution pass. + child = tailVisitor(parent, childName, child); + } + return child; + }, + ); + } + + return node; +} diff --git a/pkgs/file/lib/src/common.dart b/pkgs/file/lib/src/common.dart new file mode 100644 index 000000000..6706ec95e --- /dev/null +++ b/pkgs/file/lib/src/common.dart @@ -0,0 +1,70 @@ +// Copyright (c) 2017, 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 'interface.dart'; + +/// Returns a 'No such file or directory' [FileSystemException]. +FileSystemException noSuchFileOrDirectory(String path) { + return _fsException(path, 'No such file or directory', ErrorCodes.ENOENT); +} + +/// Returns a 'Not a directory' [FileSystemException]. +FileSystemException notADirectory(String path) { + return _fsException(path, 'Not a directory', ErrorCodes.ENOTDIR); +} + +/// Returns a 'Is a directory' [FileSystemException]. +FileSystemException isADirectory(String path) { + return _fsException(path, 'Is a directory', ErrorCodes.EISDIR); +} + +/// Returns a 'Directory not empty' [FileSystemException]. +FileSystemException directoryNotEmpty(String path) { + return _fsException(path, 'Directory not empty', ErrorCodes.ENOTEMPTY); +} + +/// Returns a 'File exists' [FileSystemException]. +FileSystemException fileExists(String path) { + return _fsException(path, 'File exists', ErrorCodes.EEXIST); +} + +/// Returns a 'Invalid argument' [FileSystemException]. +FileSystemException invalidArgument(String path) { + return _fsException(path, 'Invalid argument', ErrorCodes.EINVAL); +} + +/// Returns a 'Too many levels of symbolic links' [FileSystemException]. +FileSystemException tooManyLevelsOfSymbolicLinks(String path) { + // TODO(tvolkert): Switch to ErrorCodes.EMLINK + return _fsException( + path, 'Too many levels of symbolic links', ErrorCodes.ELOOP); +} + +/// Returns a 'Bad file descriptor' [FileSystemException]. +FileSystemException badFileDescriptor(String path) { + return _fsException(path, 'Bad file descriptor', ErrorCodes.EBADF); +} + +FileSystemException _fsException(String path, String msg, int errorCode) { + return FileSystemException(msg, path, OSError(msg, errorCode)); +} + +/// Mixin containing implementations of [Directory] methods that are common +/// to all implementations. +mixin DirectoryAddOnsMixin implements Directory { + @override + Directory childDirectory(String basename) { + return fileSystem.directory(fileSystem.path.join(path, basename)); + } + + @override + File childFile(String basename) { + return fileSystem.file(fileSystem.path.join(path, basename)); + } + + @override + Link childLink(String basename) { + return fileSystem.link(fileSystem.path.join(path, basename)); + } +} diff --git a/pkgs/file/lib/src/forwarding.dart b/pkgs/file/lib/src/forwarding.dart new file mode 100644 index 000000000..9566a3013 --- /dev/null +++ b/pkgs/file/lib/src/forwarding.dart @@ -0,0 +1,10 @@ +// Copyright (c) 2017, 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. + +export 'forwarding/forwarding_directory.dart'; +export 'forwarding/forwarding_file.dart'; +export 'forwarding/forwarding_file_system.dart'; +export 'forwarding/forwarding_file_system_entity.dart'; +export 'forwarding/forwarding_link.dart'; +export 'forwarding/forwarding_random_access_file.dart'; diff --git a/pkgs/file/lib/src/forwarding/forwarding_directory.dart b/pkgs/file/lib/src/forwarding/forwarding_directory.dart new file mode 100644 index 000000000..dba0c8ed6 --- /dev/null +++ b/pkgs/file/lib/src/forwarding/forwarding_directory.dart @@ -0,0 +1,57 @@ +// Copyright (c) 2017, 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:file/src/io.dart' as io; +import 'package:file/file.dart'; + +/// A directory that forwards all methods and properties to a delegate. +mixin ForwardingDirectory + implements ForwardingFileSystemEntity, Directory { + @override + T wrap(io.Directory delegate) => wrapDirectory(delegate) as T; + + @override + Future create({bool recursive = false}) async => + wrap(await delegate.create(recursive: recursive)); + + @override + void createSync({bool recursive = false}) => + delegate.createSync(recursive: recursive); + + @override + Future createTemp([String? prefix]) async => + wrap(await delegate.createTemp(prefix)); + + @override + Directory createTempSync([String? prefix]) => + wrap(delegate.createTempSync(prefix)); + + @override + Stream list({ + bool recursive = false, + bool followLinks = true, + }) => + delegate.list(recursive: recursive, followLinks: followLinks).map(_wrap); + + @override + List listSync({ + bool recursive = false, + bool followLinks = true, + }) => + delegate + .listSync(recursive: recursive, followLinks: followLinks) + .map(_wrap) + .toList(); + + FileSystemEntity _wrap(io.FileSystemEntity entity) { + if (entity is io.File) { + return wrapFile(entity); + } else if (entity is io.Directory) { + return wrapDirectory(entity); + } else if (entity is io.Link) { + return wrapLink(entity); + } + throw FileSystemException('Unsupported type: $entity', entity.path); + } +} diff --git a/pkgs/file/lib/src/forwarding/forwarding_file.dart b/pkgs/file/lib/src/forwarding/forwarding_file.dart new file mode 100644 index 000000000..49c211db7 --- /dev/null +++ b/pkgs/file/lib/src/forwarding/forwarding_file.dart @@ -0,0 +1,156 @@ +// Copyright (c) 2017, 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:convert'; +import 'dart:typed_data'; + +import 'package:file/src/io.dart' as io; +import 'package:file/file.dart'; + +/// A file that forwards all methods and properties to a delegate. +mixin ForwardingFile + implements ForwardingFileSystemEntity, File { + @override + ForwardingFile wrap(io.File delegate) => wrapFile(delegate) as ForwardingFile; + + // TODO(dartbug.com/49647): Pass `exclusive` through after it lands. + @override + Future create({bool recursive = false, bool exclusive = false}) async => + wrap(await delegate.create( + recursive: recursive /*, exclusive: exclusive*/)); + + // TODO(dartbug.com/49647): Pass `exclusive` through after it lands. + @override + void createSync({bool recursive = false, bool exclusive = false}) => + delegate.createSync(recursive: recursive /*, exclusive: exclusive*/); + + @override + Future copy(String newPath) async => wrap(await delegate.copy(newPath)); + + @override + File copySync(String newPath) => wrap(delegate.copySync(newPath)); + + @override + Future length() => delegate.length(); + + @override + int lengthSync() => delegate.lengthSync(); + + @override + Future lastAccessed() => delegate.lastAccessed(); + + @override + DateTime lastAccessedSync() => delegate.lastAccessedSync(); + + @override + Future setLastAccessed(DateTime time) => + delegate.setLastAccessed(time); + + @override + void setLastAccessedSync(DateTime time) => delegate.setLastAccessedSync(time); + + @override + Future lastModified() => delegate.lastModified(); + + @override + DateTime lastModifiedSync() => delegate.lastModifiedSync(); + + @override + Future setLastModified(DateTime time) => + delegate.setLastModified(time); + + @override + void setLastModifiedSync(DateTime time) => delegate.setLastModifiedSync(time); + + @override + Future open({ + FileMode mode = FileMode.read, + }) => + delegate.open(mode: mode); + + @override + RandomAccessFile openSync({FileMode mode = FileMode.read}) => + delegate.openSync(mode: mode); + + @override + Stream> openRead([int? start, int? end]) => + delegate.openRead(start, end); + + @override + IOSink openWrite({ + FileMode mode = FileMode.write, + Encoding encoding = utf8, + }) => + delegate.openWrite(mode: mode, encoding: encoding); + + @override + Future readAsBytes() => delegate.readAsBytes(); + + @override + Uint8List readAsBytesSync() => delegate.readAsBytesSync(); + + @override + Future readAsString({Encoding encoding = utf8}) => + delegate.readAsString(encoding: encoding); + + @override + String readAsStringSync({Encoding encoding = utf8}) => + delegate.readAsStringSync(encoding: encoding); + + @override + Future> readAsLines({Encoding encoding = utf8}) => + delegate.readAsLines(encoding: encoding); + + @override + List readAsLinesSync({Encoding encoding = utf8}) => + delegate.readAsLinesSync(encoding: encoding); + + @override + Future writeAsBytes( + List bytes, { + FileMode mode = FileMode.write, + bool flush = false, + }) async => + wrap(await delegate.writeAsBytes( + bytes, + mode: mode, + flush: flush, + )); + + @override + void writeAsBytesSync( + List bytes, { + FileMode mode = FileMode.write, + bool flush = false, + }) => + delegate.writeAsBytesSync(bytes, mode: mode, flush: flush); + + @override + Future writeAsString( + String contents, { + FileMode mode = FileMode.write, + Encoding encoding = utf8, + bool flush = false, + }) async => + wrap(await delegate.writeAsString( + contents, + mode: mode, + encoding: encoding, + flush: flush, + )); + + @override + void writeAsStringSync( + String contents, { + FileMode mode = FileMode.write, + Encoding encoding = utf8, + bool flush = false, + }) => + delegate.writeAsStringSync( + contents, + mode: mode, + encoding: encoding, + flush: flush, + ); +} diff --git a/pkgs/file/lib/src/forwarding/forwarding_file_system.dart b/pkgs/file/lib/src/forwarding/forwarding_file_system.dart new file mode 100644 index 000000000..d864db94c --- /dev/null +++ b/pkgs/file/lib/src/forwarding/forwarding_file_system.dart @@ -0,0 +1,66 @@ +// Copyright (c) 2017, 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:file/src/io.dart' as io; +import 'package:file/file.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; + +/// A file system that forwards all methods and properties to a delegate. +abstract class ForwardingFileSystem extends FileSystem { + /// Creates a new [ForwardingFileSystem] that forwards all methods and + /// properties to the specified [delegate]. + ForwardingFileSystem(this.delegate); + + /// The file system to which this file system will forward all activity. + @protected + final FileSystem delegate; + + @override + Directory directory(dynamic path) => delegate.directory(path); + + @override + File file(dynamic path) => delegate.file(path); + + @override + Link link(dynamic path) => delegate.link(path); + + @override + p.Context get path => delegate.path; + + @override + Directory get systemTempDirectory => delegate.systemTempDirectory; + + @override + Directory get currentDirectory => delegate.currentDirectory; + + @override + set currentDirectory(dynamic path) => delegate.currentDirectory = path; + + @override + Future stat(String path) => delegate.stat(path); + + @override + io.FileStat statSync(String path) => delegate.statSync(path); + + @override + Future identical(String path1, String path2) => + delegate.identical(path1, path2); + + @override + bool identicalSync(String path1, String path2) => + delegate.identicalSync(path1, path2); + + @override + bool get isWatchSupported => delegate.isWatchSupported; + + @override + Future type(String path, + {bool followLinks = true}) => + delegate.type(path, followLinks: followLinks); + + @override + io.FileSystemEntityType typeSync(String path, {bool followLinks = true}) => + delegate.typeSync(path, followLinks: followLinks); +} diff --git a/pkgs/file/lib/src/forwarding/forwarding_file_system_entity.dart b/pkgs/file/lib/src/forwarding/forwarding_file_system_entity.dart new file mode 100644 index 000000000..3c41b39b4 --- /dev/null +++ b/pkgs/file/lib/src/forwarding/forwarding_file_system_entity.dart @@ -0,0 +1,96 @@ +// Copyright (c) 2017, 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:file/src/io.dart' as io; +import 'package:file/file.dart'; +import 'package:meta/meta.dart'; + +/// A file system entity that forwards all methods and properties to a delegate. +abstract class ForwardingFileSystemEntity implements FileSystemEntity { + /// The entity to which this entity will forward all methods and properties. + @protected + D get delegate; + + /// Creates a new entity with the same file system and same type as this + /// entity but backed by the specified delegate. + @protected + T wrap(D delegate); + + /// Creates a new directory with the same file system as this entity and + /// backed by the specified delegate. + @protected + Directory wrapDirectory(io.Directory delegate); + + /// Creates a new file with the same file system as this entity and + /// backed by the specified delegate. + @protected + File wrapFile(io.File delegate); + + /// Creates a new link with the same file system as this entity and + /// backed by the specified delegate. + @protected + Link wrapLink(io.Link delegate); + + @override + Uri get uri => delegate.uri; + + @override + Future exists() => delegate.exists(); + + @override + bool existsSync() => delegate.existsSync(); + + @override + Future rename(String newPath) async => + wrap(await delegate.rename(newPath) as D); + + @override + T renameSync(String newPath) => wrap(delegate.renameSync(newPath) as D); + + @override + Future resolveSymbolicLinks() => delegate.resolveSymbolicLinks(); + + @override + String resolveSymbolicLinksSync() => delegate.resolveSymbolicLinksSync(); + + @override + Future stat() => delegate.stat(); + + @override + io.FileStat statSync() => delegate.statSync(); + + @override + Future delete({bool recursive = false}) async => + wrap(await delegate.delete(recursive: recursive) as D); + + @override + void deleteSync({bool recursive = false}) => + delegate.deleteSync(recursive: recursive); + + @override + Stream watch({ + int events = FileSystemEvent.all, + bool recursive = false, + }) => + delegate.watch(events: events, recursive: recursive); + + @override + bool get isAbsolute => delegate.isAbsolute; + + @override + T get absolute => wrap(delegate.absolute as D); + + @override + Directory get parent => wrapDirectory(delegate.parent); + + @override + String get path => delegate.path; + + @override + String get basename => fileSystem.path.basename(path); + + @override + String get dirname => fileSystem.path.dirname(path); +} diff --git a/pkgs/file/lib/src/forwarding/forwarding_link.dart b/pkgs/file/lib/src/forwarding/forwarding_link.dart new file mode 100644 index 000000000..7a60ecbfe --- /dev/null +++ b/pkgs/file/lib/src/forwarding/forwarding_link.dart @@ -0,0 +1,34 @@ +// Copyright (c) 2017, 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:file/src/io.dart' as io; +import 'package:file/file.dart'; + +/// A link that forwards all methods and properties to a delegate. +mixin ForwardingLink + implements ForwardingFileSystemEntity, Link { + @override + ForwardingLink wrap(io.Link delegate) => wrapLink(delegate) as ForwardingLink; + + @override + Future create(String target, {bool recursive = false}) async => + wrap(await delegate.create(target, recursive: recursive)); + + @override + void createSync(String target, {bool recursive = false}) => + delegate.createSync(target, recursive: recursive); + + @override + Future update(String target) async => + wrap(await delegate.update(target)); + + @override + void updateSync(String target) => delegate.updateSync(target); + + @override + Future target() => delegate.target(); + + @override + String targetSync() => delegate.targetSync(); +} diff --git a/pkgs/file/lib/src/forwarding/forwarding_random_access_file.dart b/pkgs/file/lib/src/forwarding/forwarding_random_access_file.dart new file mode 100644 index 000000000..9dd407930 --- /dev/null +++ b/pkgs/file/lib/src/forwarding/forwarding_random_access_file.dart @@ -0,0 +1,149 @@ +// Copyright (c) 2020, 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:convert'; +import 'dart:typed_data'; + +import 'package:file/src/io.dart' as io; +import 'package:meta/meta.dart'; + +/// A [RandomAccessFile] implementation that forwards all methods and properties +/// to a delegate. +mixin ForwardingRandomAccessFile implements io.RandomAccessFile { + /// The entity to which this entity will forward all methods and properties. + @protected + io.RandomAccessFile get delegate; + + @override + String get path => delegate.path; + + @override + Future close() => delegate.close(); + + @override + void closeSync() => delegate.closeSync(); + + @override + Future flush() async { + await delegate.flush(); + return this; + } + + @override + void flushSync() => delegate.flushSync(); + + @override + Future length() => delegate.length(); + + @override + int lengthSync() => delegate.lengthSync(); + + @override + Future lock([ + io.FileLock mode = io.FileLock.exclusive, + int start = 0, + int end = -1, + ]) async { + await delegate.lock(mode, start, end); + return this; + } + + @override + void lockSync([ + io.FileLock mode = io.FileLock.exclusive, + int start = 0, + int end = -1, + ]) => + delegate.lockSync(mode, start, end); + + @override + Future position() => delegate.position(); + + @override + int positionSync() => delegate.positionSync(); + + @override + Future read(int bytes) => delegate.read(bytes); + + @override + Uint8List readSync(int bytes) => delegate.readSync(bytes); + + @override + Future readByte() => delegate.readByte(); + + @override + int readByteSync() => delegate.readByteSync(); + + @override + Future readInto(List buffer, [int start = 0, int? end]) => + delegate.readInto(buffer, start, end); + + @override + int readIntoSync(List buffer, [int start = 0, int? end]) => + delegate.readIntoSync(buffer, start, end); + + @override + Future setPosition(int position) async { + await delegate.setPosition(position); + return this; + } + + @override + void setPositionSync(int position) => delegate.setPositionSync(position); + + @override + Future truncate(int length) async { + await delegate.truncate(length); + return this; + } + + @override + void truncateSync(int length) => delegate.truncateSync(length); + + @override + Future unlock([int start = 0, int end = -1]) async { + await delegate.unlock(start, end); + return this; + } + + @override + void unlockSync([int start = 0, int end = -1]) => + delegate.unlockSync(start, end); + + @override + Future writeByte(int value) async { + await delegate.writeByte(value); + return this; + } + + @override + int writeByteSync(int value) => delegate.writeByteSync(value); + + @override + Future writeFrom( + List buffer, [ + int start = 0, + int? end, + ]) async { + await delegate.writeFrom(buffer, start, end); + return this; + } + + @override + void writeFromSync(List buffer, [int start = 0, int? end]) => + delegate.writeFromSync(buffer, start, end); + + @override + Future writeString( + String string, { + Encoding encoding = utf8, + }) async { + await delegate.writeString(string, encoding: encoding); + return this; + } + + @override + void writeStringSync(String string, {Encoding encoding = utf8}) => + delegate.writeStringSync(string, encoding: encoding); +} diff --git a/pkgs/file/lib/src/interface.dart b/pkgs/file/lib/src/interface.dart new file mode 100644 index 000000000..4662e3515 --- /dev/null +++ b/pkgs/file/lib/src/interface.dart @@ -0,0 +1,13 @@ +// Copyright (c) 2017, 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. + +library file.src.interface; + +export 'interface/directory.dart'; +export 'interface/error_codes.dart'; +export 'interface/file.dart'; +export 'interface/file_system.dart'; +export 'interface/file_system_entity.dart'; +export 'interface/link.dart'; +export 'io.dart' hide Directory, File, FileSystemEntity, Link; diff --git a/pkgs/file/lib/src/interface/directory.dart b/pkgs/file/lib/src/interface/directory.dart new file mode 100644 index 000000000..e62ad8c18 --- /dev/null +++ b/pkgs/file/lib/src/interface/directory.dart @@ -0,0 +1,51 @@ +// Copyright (c) 2017, 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 '../io.dart' as io; + +import 'file.dart'; +import 'file_system_entity.dart'; +import 'link.dart'; + +/// A reference to a directory on the file system. +abstract class Directory implements FileSystemEntity, io.Directory { + // Override method definitions to codify the return type covariance. + @override + Future create({bool recursive = false}); + + @override + Future createTemp([String? prefix]); + + @override + Directory createTempSync([String? prefix]); + + @override + Future rename(String newPath); + + @override + Directory renameSync(String newPath); + + @override + Directory get absolute; + + @override + Stream list( + {bool recursive = false, bool followLinks = true}); + + @override + List listSync( + {bool recursive = false, bool followLinks = true}); + + /// Returns a reference to a [Directory] that exists as a child of this + /// directory and has the specified [basename]. + Directory childDirectory(String basename); + + /// Returns a reference to a [File] that exists as a child of this directory + /// and has the specified [basename]. + File childFile(String basename); + + /// Returns a reference to a [Link] that exists as a child of this directory + /// and has the specified [basename]. + Link childLink(String basename); +} diff --git a/pkgs/file/lib/src/interface/error_codes.dart b/pkgs/file/lib/src/interface/error_codes.dart new file mode 100644 index 000000000..8943538cb --- /dev/null +++ b/pkgs/file/lib/src/interface/error_codes.dart @@ -0,0 +1,585 @@ +// Copyright (c) 2017, 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 'error_codes_internal.dart' + if (dart.library.io) 'error_codes_dart_io.dart'; + +/// Operating system error codes. +// TODO(tvolkert): Remove (https://github.com/dart-lang/sdk/issues/28860) +class ErrorCodes { + ErrorCodes._(); + + /// Argument list too long + // ignore: non_constant_identifier_names + static int get E2BIG => _platform((_Codes codes) => codes.e2big); + + /// Permission denied + // ignore: non_constant_identifier_names + static int get EACCES => _platform((_Codes codes) => codes.eacces); + + /// Try again + // ignore: non_constant_identifier_names + static int get EAGAIN => _platform((_Codes codes) => codes.eagain); + + /// Bad file number + // ignore: non_constant_identifier_names + static int get EBADF => _platform((_Codes codes) => codes.ebadf); + + /// Device or resource busy + // ignore: non_constant_identifier_names + static int get EBUSY => _platform((_Codes codes) => codes.ebusy); + + /// No child processes + // ignore: non_constant_identifier_names + static int get ECHILD => _platform((_Codes codes) => codes.echild); + + /// Resource deadlock would occur + // ignore: non_constant_identifier_names + static int get EDEADLK => _platform((_Codes codes) => codes.edeadlk); + + /// Math argument out of domain of func + // ignore: non_constant_identifier_names + static int get EDOM => _platform((_Codes codes) => codes.edom); + + /// File exists + // ignore: non_constant_identifier_names + static int get EEXIST => _platform((_Codes codes) => codes.eexist); + + /// Bad address + // ignore: non_constant_identifier_names + static int get EFAULT => _platform((_Codes codes) => codes.efault); + + /// File too large + // ignore: non_constant_identifier_names + static int get EFBIG => _platform((_Codes codes) => codes.efbig); + + /// Illegal byte sequence + // ignore: non_constant_identifier_names + static int get EILSEQ => _platform((_Codes codes) => codes.eilseq); + + /// Interrupted system call + // ignore: non_constant_identifier_names + static int get EINTR => _platform((_Codes codes) => codes.eintr); + + /// Invalid argument + // ignore: non_constant_identifier_names + static int get EINVAL => _platform((_Codes codes) => codes.einval); + + /// I/O error + // ignore: non_constant_identifier_names + static int get EIO => _platform((_Codes codes) => codes.eio); + + /// Is a directory + // ignore: non_constant_identifier_names + static int get EISDIR => _platform((_Codes codes) => codes.eisdir); + + /// Too many levels of symbolic links + // ignore: non_constant_identifier_names + static int get ELOOP => _platform((_Codes codes) => codes.eloop); + + /// Too many open files + // ignore: non_constant_identifier_names + static int get EMFILE => _platform((_Codes codes) => codes.emfile); + + /// Too many links + // ignore: non_constant_identifier_names + static int get EMLINK => _platform((_Codes codes) => codes.emlink); + + /// File name too long + // ignore: non_constant_identifier_names + static int get ENAMETOOLONG => + _platform((_Codes codes) => codes.enametoolong); + + /// File table overflow + // ignore: non_constant_identifier_names + static int get ENFILE => _platform((_Codes codes) => codes.enfile); + + /// No such device + // ignore: non_constant_identifier_names + static int get ENODEV => _platform((_Codes codes) => codes.enodev); + + /// No such file or directory + // ignore: non_constant_identifier_names + static int get ENOENT => _platform((_Codes codes) => codes.enoent); + + /// Exec format error + // ignore: non_constant_identifier_names + static int get ENOEXEC => _platform((_Codes codes) => codes.enoexec); + + /// No record locks available + // ignore: non_constant_identifier_names + static int get ENOLCK => _platform((_Codes codes) => codes.enolck); + + /// Out of memory + // ignore: non_constant_identifier_names + static int get ENOMEM => _platform((_Codes codes) => codes.enomem); + + /// No space left on device + // ignore: non_constant_identifier_names + static int get ENOSPC => _platform((_Codes codes) => codes.enospc); + + /// Function not implemented + // ignore: non_constant_identifier_names + static int get ENOSYS => _platform((_Codes codes) => codes.enosys); + + /// Not a directory + // ignore: non_constant_identifier_names + static int get ENOTDIR => _platform((_Codes codes) => codes.enotdir); + + /// Directory not empty + // ignore: non_constant_identifier_names + static int get ENOTEMPTY => _platform((_Codes codes) => codes.enotempty); + + /// Not a typewriter + // ignore: non_constant_identifier_names + static int get ENOTTY => _platform((_Codes codes) => codes.enotty); + + /// No such device or address + // ignore: non_constant_identifier_names + static int get ENXIO => _platform((_Codes codes) => codes.enxio); + + /// Operation not permitted + // ignore: non_constant_identifier_names + static int get EPERM => _platform((_Codes codes) => codes.eperm); + + /// Broken pipe + // ignore: non_constant_identifier_names + static int get EPIPE => _platform((_Codes codes) => codes.epipe); + + /// Math result not representable + // ignore: non_constant_identifier_names + static int get ERANGE => _platform((_Codes codes) => codes.erange); + + /// Read-only file system + // ignore: non_constant_identifier_names + static int get EROFS => _platform((_Codes codes) => codes.erofs); + + /// Illegal seek + // ignore: non_constant_identifier_names + static int get ESPIPE => _platform((_Codes codes) => codes.espipe); + + /// No such process + // ignore: non_constant_identifier_names + static int get ESRCH => _platform((_Codes codes) => codes.esrch); + + /// Cross-device link + // ignore: non_constant_identifier_names + static int get EXDEV => _platform((_Codes codes) => codes.exdev); + + static int _platform(int Function(_Codes codes) getCode) { + _Codes codes = (_platforms[operatingSystem] ?? _platforms['linux'])!; + return getCode(codes); + } +} + +const Map _platforms = { + 'linux': _LinuxCodes(), + 'macos': _MacOSCodes(), + 'windows': _WindowsCodes(), +}; + +abstract class _Codes { + int get e2big; + int get eacces; + int get eagain; + int get ebadf; + int get ebusy; + int get echild; + int get edeadlk; + int get edom; + int get eexist; + int get efault; + int get efbig; + int get eilseq; + int get eintr; + int get einval; + int get eio; + int get eisdir; + int get eloop; + int get emfile; + int get emlink; + int get enametoolong; + int get enfile; + int get enodev; + int get enoent; + int get enoexec; + int get enolck; + int get enomem; + int get enospc; + int get enosys; + int get enotdir; + int get enotempty; + int get enotty; + int get enxio; + int get eperm; + int get epipe; + int get erange; + int get erofs; + int get espipe; + int get esrch; + int get exdev; +} + +class _LinuxCodes implements _Codes { + const _LinuxCodes(); + + @override + int get e2big => 7; + + @override + int get eacces => 13; + + @override + int get eagain => 11; + + @override + int get ebadf => 9; + + @override + int get ebusy => 16; + + @override + int get echild => 10; + + @override + int get edeadlk => 35; + + @override + int get edom => 33; + + @override + int get eexist => 17; + + @override + int get efault => 14; + + @override + int get efbig => 27; + + @override + int get eilseq => 84; + + @override + int get eintr => 4; + + @override + int get einval => 22; + + @override + int get eio => 5; + + @override + int get eisdir => 21; + + @override + int get eloop => 40; + + @override + int get emfile => 24; + + @override + int get emlink => 31; + + @override + int get enametoolong => 36; + + @override + int get enfile => 23; + + @override + int get enodev => 19; + + @override + int get enoent => 2; + + @override + int get enoexec => 8; + + @override + int get enolck => 37; + + @override + int get enomem => 12; + + @override + int get enospc => 28; + + @override + int get enosys => 38; + + @override + int get enotdir => 20; + + @override + int get enotempty => 39; + + @override + int get enotty => 25; + + @override + int get enxio => 6; + + @override + int get eperm => 1; + + @override + int get epipe => 32; + + @override + int get erange => 34; + + @override + int get erofs => 30; + + @override + int get espipe => 29; + + @override + int get esrch => 3; + + @override + int get exdev => 18; +} + +class _MacOSCodes implements _Codes { + const _MacOSCodes(); + + @override + int get e2big => 7; + + @override + int get eacces => 13; + + @override + int get eagain => 35; + + @override + int get ebadf => 9; + + @override + int get ebusy => 16; + + @override + int get echild => 10; + + @override + int get edeadlk => 11; + + @override + int get edom => 33; + + @override + int get eexist => 17; + + @override + int get efault => 14; + + @override + int get efbig => 27; + + @override + int get eilseq => 92; + + @override + int get eintr => 4; + + @override + int get einval => 22; + + @override + int get eio => 5; + + @override + int get eisdir => 21; + + @override + int get eloop => 62; + + @override + int get emfile => 24; + + @override + int get emlink => 31; + + @override + int get enametoolong => 63; + + @override + int get enfile => 23; + + @override + int get enodev => 19; + + @override + int get enoent => 2; + + @override + int get enoexec => 8; + + @override + int get enolck => 77; + + @override + int get enomem => 12; + + @override + int get enospc => 28; + + @override + int get enosys => 78; + + @override + int get enotdir => 20; + + @override + int get enotempty => 66; + + @override + int get enotty => 25; + + @override + int get enxio => 6; + + @override + int get eperm => 1; + + @override + int get epipe => 32; + + @override + int get erange => 34; + + @override + int get erofs => 30; + + @override + int get espipe => 29; + + @override + int get esrch => 3; + + @override + int get exdev => 18; +} + +class _WindowsCodes implements _Codes { + const _WindowsCodes(); + + @override + int get e2big => 7; + + @override + int get eacces => 13; + + @override + int get eagain => 11; + + @override + int get ebadf => 9; + + @override + int get ebusy => 16; + + @override + int get echild => 10; + + @override + int get edeadlk => 36; + + @override + int get edom => 33; + + @override + int get eexist => 17; + + @override + int get efault => 14; + + @override + int get efbig => 27; + + @override + int get eilseq => 42; + + @override + int get eintr => 4; + + @override + int get einval => 22; + + @override + int get eio => 5; + + @override + int get eisdir => 21; + + @override + int get eloop => -1; + + @override + int get emfile => 24; + + @override + int get emlink => 31; + + @override + int get enametoolong => 38; + + @override + int get enfile => 23; + + @override + int get enodev => 19; + + @override + int get enoent => 2; + + @override + int get enoexec => 8; + + @override + int get enolck => 39; + + @override + int get enomem => 12; + + @override + int get enospc => 28; + + @override + int get enosys => 40; + + @override + int get enotdir => 20; + + @override + int get enotempty => 41; + + @override + int get enotty => 25; + + @override + int get enxio => 6; + + @override + int get eperm => 1; + + @override + int get epipe => 32; + + @override + int get erange => 34; + + @override + int get erofs => 30; + + @override + int get espipe => 29; + + @override + int get esrch => 3; + + @override + int get exdev => 18; +} diff --git a/pkgs/file/lib/src/interface/error_codes_dart_io.dart b/pkgs/file/lib/src/interface/error_codes_dart_io.dart new file mode 100644 index 000000000..3f0a97f1f --- /dev/null +++ b/pkgs/file/lib/src/interface/error_codes_dart_io.dart @@ -0,0 +1,10 @@ +// Copyright (c) 2017, 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:io' show Platform; + +/// If we have `dart:io` available, we pull the current operating system from +/// the [Platform] class, so we'll get errno values that match our current +/// operating system. +final String operatingSystem = Platform.operatingSystem; diff --git a/pkgs/file/lib/src/interface/error_codes_internal.dart b/pkgs/file/lib/src/interface/error_codes_internal.dart new file mode 100644 index 000000000..0a9d7dcf3 --- /dev/null +++ b/pkgs/file/lib/src/interface/error_codes_internal.dart @@ -0,0 +1,8 @@ +// Copyright (c) 2017, 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. + +/// In environments that don't have `dart:io`, we can't access the Platform +/// class to determine what platform we're on, so we just pretend we're on +/// Linux, meaning we'll get errno values that match Linux's errno.h. +const String operatingSystem = 'linux'; diff --git a/pkgs/file/lib/src/interface/file.dart b/pkgs/file/lib/src/interface/file.dart new file mode 100644 index 000000000..87417c64f --- /dev/null +++ b/pkgs/file/lib/src/interface/file.dart @@ -0,0 +1,41 @@ +// Copyright (c) 2017, 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:convert'; + +import '../io.dart' as io; + +import 'file_system_entity.dart'; + +/// A reference to a file on the file system. +abstract class File implements FileSystemEntity, io.File { + // Override method definitions to codify the return type covariance. + @override + Future create({bool recursive = false, bool exclusive = false}); + + @override + Future rename(String newPath); + + @override + File renameSync(String newPath); + + @override + Future copy(String newPath); + + @override + File copySync(String newPath); + + @override + File get absolute; + + @override + Future writeAsBytes(List bytes, + {io.FileMode mode = io.FileMode.write, bool flush = false}); + + @override + Future writeAsString(String contents, + {io.FileMode mode = io.FileMode.write, + Encoding encoding = utf8, + bool flush = false}); +} diff --git a/pkgs/file/lib/src/interface/file_system.dart b/pkgs/file/lib/src/interface/file_system.dart new file mode 100644 index 000000000..4fd528df9 --- /dev/null +++ b/pkgs/file/lib/src/interface/file_system.dart @@ -0,0 +1,163 @@ +// Copyright (c) 2017, 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:meta/meta.dart'; +import 'package:path/path.dart' as p; + +import '../io.dart' as io; + +import 'directory.dart'; +import 'file.dart'; +import 'file_system_entity.dart'; +import 'link.dart'; + +/// A generic representation of a file system. +/// +/// Note that this class uses `dart:io` only inasmuch as it deals in the types +/// exposed by the `dart:io` library. Subclasses should document their level of +/// dependence on the library (and the associated implications of using that +/// implementation in the browser). +abstract class FileSystem { + /// Creates a new `FileSystem`. + const FileSystem(); + + /// Returns a reference to a [Directory] at [path]. + /// + /// [path] can be either a [`String`], a [`Uri`], or a [`FileSystemEntity`]. + Directory directory(dynamic path); + + /// Returns a reference to a [File] at [path]. + /// + /// [path] can be either a [`String`], a [`Uri`], or a [`FileSystemEntity`]. + File file(dynamic path); + + /// Returns a reference to a [Link] at [path]. + /// + /// [path] can be either a [`String`], a [`Uri`], or a [`FileSystemEntity`]. + Link link(dynamic path); + + /// An object for manipulating paths in this file system. + p.Context get path; + + /// Gets the system temp directory. + /// + /// It is left to file system implementations to decide how to define the + /// "system temp directory". + Directory get systemTempDirectory; + + /// Creates a directory object pointing to the current working directory. + Directory get currentDirectory; + + /// Sets the current working directory to the specified [path]. + /// + /// The new value set can be either a [Directory] or a [String]. + /// + /// Relative paths will be resolved by the underlying file system + /// implementation (meaning it is up to the underlying implementation to + /// decide whether to support relative paths). + set currentDirectory(dynamic path); + + /// Asynchronously calls the operating system's stat() function on [path]. + /// Returns a Future which completes with a [io.FileStat] object containing + /// the data returned by stat(). + /// If the call fails, completes the future with a [io.FileStat] object with + /// .type set to FileSystemEntityType.NOT_FOUND and the other fields invalid. + Future stat(String path); + + /// Calls the operating system's stat() function on [path]. + /// Returns a [io.FileStat] object containing the data returned by stat(). + /// If the call fails, returns a [io.FileStat] object with .type set to + /// FileSystemEntityType.NOT_FOUND and the other fields invalid. + io.FileStat statSync(String path); + + /// Checks whether two paths refer to the same object in the + /// file system. Returns a [Future] that completes with the result. + /// + /// Comparing a link to its target returns false, as does comparing two links + /// that point to the same target. To check the target of a link, use + /// Link.target explicitly to fetch it. Directory links appearing + /// inside a path are followed, though, to find the file system object. + /// + /// Completes the returned Future with an error if one of the paths points + /// to an object that does not exist. + Future identical(String path1, String path2); + + /// Synchronously checks whether two paths refer to the same object in the + /// file system. + /// + /// Comparing a link to its target returns false, as does comparing two links + /// that point to the same target. To check the target of a link, use + /// Link.target explicitly to fetch it. Directory links appearing + /// inside a path are followed, though, to find the file system object. + /// + /// Throws an error if one of the paths points to an object that does not + /// exist. + bool identicalSync(String path1, String path2); + + /// Tests if [FileSystemEntity.watch] is supported on the current system. + bool get isWatchSupported; + + /// Finds the type of file system object that a [path] points to. Returns + /// a Future that completes with the result. + /// + /// [io.FileSystemEntityType.LINK] will only be returned if [followLinks] is + /// `false`, and [path] points to a link + /// + /// If the [path] does not point to a file system object or an error occurs + /// then [io.FileSystemEntityType.notFound] is returned. + Future type(String path, {bool followLinks = true}); + + /// Syncronously finds the type of file system object that a [path] points + /// to. Returns a [io.FileSystemEntityType]. + /// + /// [io.FileSystemEntityType.LINK] will only be returned if [followLinks] is + /// `false`, and [path] points to a link + /// + /// If the [path] does not point to a file system object or an error occurs + /// then [io.FileSystemEntityType.notFound] is returned. + io.FileSystemEntityType typeSync(String path, {bool followLinks = true}); + + /// Checks if [`type(path)`](type) returns [io.FileSystemEntityType.FILE]. + Future isFile(String path) async => + await type(path) == io.FileSystemEntityType.file; + + /// Synchronously checks if [`type(path)`](type) returns + /// [io.FileSystemEntityType.FILE]. + bool isFileSync(String path) => + typeSync(path) == io.FileSystemEntityType.file; + + /// Checks if [`type(path)`](type) returns [io.FileSystemEntityType.DIRECTORY]. + Future isDirectory(String path) async => + await type(path) == io.FileSystemEntityType.directory; + + /// Synchronously checks if [`type(path)`](type) returns + /// [io.FileSystemEntityType.DIRECTORY]. + bool isDirectorySync(String path) => + typeSync(path) == io.FileSystemEntityType.directory; + + /// Checks if [`type(path)`](type) returns [io.FileSystemEntityType.LINK]. + Future isLink(String path) async => + await type(path, followLinks: false) == io.FileSystemEntityType.link; + + /// Synchronously checks if [`type(path)`](type) returns + /// [io.FileSystemEntityType.LINK]. + bool isLinkSync(String path) => + typeSync(path, followLinks: false) == io.FileSystemEntityType.link; + + /// Gets the string path represented by the specified generic [path]. + /// + /// [path] may be a [io.FileSystemEntity], a [String], or a [Uri]. + @protected + String getPath(dynamic path) { + if (path is io.FileSystemEntity) { + return path.path; + } else if (path is String) { + return path; + } else if (path is Uri) { + return this.path.fromUri(path); + } else { + throw ArgumentError('Invalid type for "path": ${path?.runtimeType}'); + } + } +} diff --git a/pkgs/file/lib/src/interface/file_system_entity.dart b/pkgs/file/lib/src/interface/file_system_entity.dart new file mode 100644 index 000000000..a377397a8 --- /dev/null +++ b/pkgs/file/lib/src/interface/file_system_entity.dart @@ -0,0 +1,42 @@ +// Copyright (c) 2017, 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 '../io.dart' as io; + +import 'directory.dart'; +import 'file_system.dart'; + +/// The common super class for [io.File], [io.Directory], and [io.Link] objects. +abstract class FileSystemEntity implements io.FileSystemEntity { + /// Returns the file system responsible for this entity. + FileSystem get fileSystem; + + /// Gets the part of this entity's path after the last separator. + /// + /// context.basename('path/to/foo.dart'); // -> 'foo.dart' + /// context.basename('path/to'); // -> 'to' + /// + /// Trailing separators are ignored. + /// + /// context.basename('path/to/'); // -> 'to' + String get basename; + + /// Gets the part of this entity's path before the last separator. + /// + /// context.dirname('path/to/foo.dart'); // -> 'path/to' + /// context.dirname('path/to'); // -> 'path' + /// context.dirname('foo.dart'); // -> '.' + /// + /// Trailing separators are ignored. + /// + /// context.dirname('path/to/'); // -> 'path' + String get dirname; + + // Override method definitions to codify the return type covariance. + @override + Future delete({bool recursive = false}); + + @override + Directory get parent; +} diff --git a/pkgs/file/lib/src/interface/link.dart b/pkgs/file/lib/src/interface/link.dart new file mode 100644 index 000000000..27874b34a --- /dev/null +++ b/pkgs/file/lib/src/interface/link.dart @@ -0,0 +1,26 @@ +// Copyright (c) 2017, 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 '../io.dart' as io; + +import 'file_system_entity.dart'; + +/// A reference to a symbolic link on the file system. +abstract class Link implements FileSystemEntity, io.Link { + // Override method definitions to codify the return type covariance. + @override + Future create(String target, {bool recursive = false}); + + @override + Future update(String target); + + @override + Future rename(String newPath); + + @override + Link renameSync(String newPath); + + @override + Link get absolute; +} diff --git a/pkgs/file/lib/src/io.dart b/pkgs/file/lib/src/io.dart new file mode 100644 index 000000000..9d57e7869 --- /dev/null +++ b/pkgs/file/lib/src/io.dart @@ -0,0 +1,26 @@ +// Copyright (c) 2017, 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. + +/// For internal use only! +/// +/// This exposes the subset of the `dart:io` interfaces that are required by +/// the `file` package. The `file` package re-exports these interfaces (or in +/// some cases, implementations of these interfaces by the same name), so this +/// file need not be exposes publicly and exists for internal use only. +export 'dart:io' + show + Directory, + File, + FileLock, + FileMode, + FileStat, + FileSystemEntity, + FileSystemEntityType, + FileSystemEvent, + FileSystemException, + IOException, + IOSink, + Link, + OSError, + RandomAccessFile; diff --git a/pkgs/file/pubspec.yaml b/pkgs/file/pubspec.yaml new file mode 100644 index 000000000..ae7db0ba9 --- /dev/null +++ b/pkgs/file/pubspec.yaml @@ -0,0 +1,19 @@ +name: file +version: 7.0.1-wip +description: + A pluggable, mockable file system abstraction for Dart. Supports local file + system access, as well as in-memory file systems, record-replay file systems, + and chroot file systems. +repository: https://github.com/dart-lang/tools/tree/main/pkgs/file + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + meta: ^1.9.1 + path: ^1.8.3 + +dev_dependencies: + file_testing: ^3.0.0 + lints: ^2.0.1 + test: ^1.23.1 diff --git a/pkgs/file/test/chroot_test.dart b/pkgs/file/test/chroot_test.dart new file mode 100644 index 000000000..6c34ff200 --- /dev/null +++ b/pkgs/file/test/chroot_test.dart @@ -0,0 +1,177 @@ +// Copyright (c) 2017, 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') +import 'dart:io' as io; + +import 'package:file/chroot.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:file/memory.dart'; +import 'package:file_testing/file_testing.dart'; +import 'package:test/test.dart'; + +import 'common_tests.dart'; + +void main() { + group('ChrootFileSystem', () { + ChrootFileSystem createMemoryBackedChrootFileSystem() { + MemoryFileSystem fs = MemoryFileSystem(); + fs.directory('/tmp').createSync(); + return ChrootFileSystem(fs, '/tmp'); + } + + // TODO(jamesderlin): Make ChrootFile.openSync return a delegating + // RandomAccessFile that uses the chroot'd path. + List skipCommon = [ + 'File > open > .* > RandomAccessFile > read > openReadHandleDoesNotChange', + 'File > open > .* > RandomAccessFile > openWriteHandleDoesNotChange', + ]; + + group('memoryBacked', () { + runCommonTests(createMemoryBackedChrootFileSystem, skip: skipCommon); + }); + + group('localBacked', () { + late ChrootFileSystem fs; + late io.Directory tmp; + + setUp(() { + tmp = io.Directory.systemTemp.createTempSync('file_test_'); + tmp = io.Directory(tmp.resolveSymbolicLinksSync()); + fs = ChrootFileSystem(const LocalFileSystem(), tmp.path); + }); + + tearDown(() { + tmp.deleteSync(recursive: true); + }); + + runCommonTests( + () => fs, + skip: [ + // https://github.com/dart-lang/sdk/issues/28275 + 'Link > rename > throwsIfDestinationExistsAsDirectory', + + // https://github.com/dart-lang/sdk/issues/28277 + 'Link > rename > throwsIfDestinationExistsAsFile', + + ...skipCommon, + ], + ); + }, skip: io.Platform.isWindows); + + group('chrootSpecific', () { + late ChrootFileSystem fs; + late MemoryFileSystem mem; + + setUp(() { + fs = createMemoryBackedChrootFileSystem(); + mem = fs.delegate as MemoryFileSystem; + }); + + group('FileSystem', () { + group('currentDirectory', () { + test('staysInJailIfSetToParentOfRoot', () { + fs.currentDirectory = '../../../..'; + fs.file('foo').createSync(); + expect(mem.file('/tmp/foo'), exists); + }); + + test('throwsIfSetToSymlinkToDirectoryOutsideJail', () { + mem.directory('/bar').createSync(); + mem.link('/tmp/foo').createSync('/bar'); + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.currentDirectory = '/foo'; + }); + }); + }); + + group('stat', () { + test('isNotFoundForJailbreakPath', () { + mem.file('/foo').createSync(); + expect(fs.statSync('../foo').type, FileSystemEntityType.notFound); + }); + + test('isNotFoundForSymlinkWithJailbreakTarget', () { + mem.file('/foo').createSync(); + mem.link('/tmp/bar').createSync('/foo'); + expect(mem.statSync('/tmp/bar').type, FileSystemEntityType.file); + expect(fs.statSync('/bar').type, FileSystemEntityType.notFound); + }); + + test('isNotFoundForSymlinkToOutsideAndBackInsideJail', () { + mem.file('/tmp/bar').createSync(); + mem.link('/foo').createSync('/tmp/bar'); + mem.link('/tmp/baz').createSync('/foo'); + expect(mem.statSync('/tmp/baz').type, FileSystemEntityType.file); + expect(fs.statSync('/baz').type, FileSystemEntityType.notFound); + }); + }); + + group('type', () { + test('isNotFoundForJailbreakPath', () { + mem.file('/foo').createSync(); + expect(fs.typeSync('../foo'), FileSystemEntityType.notFound); + }); + + test('isNotFoundForSymlinkWithJailbreakTarget', () { + mem.file('/foo').createSync(); + mem.link('/tmp/bar').createSync('/foo'); + expect(mem.typeSync('/tmp/bar'), FileSystemEntityType.file); + expect(fs.typeSync('/bar'), FileSystemEntityType.notFound); + }); + + test('isNotFoundForSymlinkToOutsideAndBackInsideJail', () { + mem.file('/tmp/bar').createSync(); + mem.link('/foo').createSync('/tmp/bar'); + mem.link('/tmp/baz').createSync('/foo'); + expect(mem.typeSync('/tmp/baz'), FileSystemEntityType.file); + expect(fs.typeSync('/baz'), FileSystemEntityType.notFound); + }); + }); + }); + + group('File', () { + group('delegate', () { + test('referencesRootEntityForJailbreakPath', () { + mem.file('/foo').createSync(); + dynamic f = fs.file('../foo'); + expect(f.delegate.path, '/tmp/foo'); + }); + }); + + group('create', () { + test('createsAtRootIfPathReferencesJailbreakFile', () { + fs.file('../foo').createSync(); + expect(mem.file('/foo'), isNot(exists)); + expect(mem.file('/tmp/foo'), exists); + }); + }); + + group('copy', () { + test('copiesToRootDirectoryIfDestinationIsJailbreakPath', () { + File f = fs.file('/foo')..createSync(); + f.copySync('../bar'); + expect(mem.file('/bar'), isNot(exists)); + expect(mem.file('/tmp/bar'), exists); + }); + }); + }); + + group('Link', () { + group('target', () { + test('chrootAndDelegateFileSystemsReturnSameValue', () { + mem.file('/foo').createSync(); + mem.link('/tmp/bar').createSync('/foo'); + mem.link('/tmp/baz').createSync('../foo'); + expect(mem.link('/tmp/bar').targetSync(), '/foo'); + expect(fs.link('/bar').targetSync(), '/foo'); + expect(mem.link('/tmp/baz').targetSync(), '../foo'); + expect(fs.link('/baz').targetSync(), '../foo'); + }); + }); + }); + }); + }); +} diff --git a/pkgs/file/test/common_tests.dart b/pkgs/file/test/common_tests.dart new file mode 100644 index 000000000..6028c7715 --- /dev/null +++ b/pkgs/file/test/common_tests.dart @@ -0,0 +1,3517 @@ +// Copyright (c) 2017, 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') +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:file_testing/file_testing.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:test/test.dart' as testpkg show group, setUp, tearDown, test; + +import 'utils.dart'; + +/// Callback used in [runCommonTests] to produce the root folder in which all +/// file system entities will be created. +typedef RootPathGenerator = String Function(); + +/// Callback used in [runCommonTests] to create the file system under test. +/// It must return either a [FileSystem] or a [Future] that completes with a +/// [FileSystem]. +typedef FileSystemGenerator = dynamic Function(); + +/// A function to run before tests (passed to [setUp]) or after tests +/// (passed to [tearDown]). +typedef SetUpTearDown = dynamic Function(); + +/// Runs a suite of tests common to all file system implementations. All file +/// system implementations should run *at least* these tests to ensure +/// compliance with file system API. +/// +/// If [root] is specified, its return value will be used as the root folder +/// in which all file system entities will be created. If not specified, the +/// tests will attempt to create entities in the file system root. +/// +/// [skip] may be used to skip certain tests (or entire groups of tests) in +/// this suite (to be used, for instance, if a file system implementation is +/// not yet fully complete). The format of each entry in the list is: +/// `$group1Description > $group2Description > ... > $testDescription`. +/// Entries may use regular expression syntax. +/// +/// If [replay] is specified, each test (and its setup callbacks) will run +/// twice - once as a "setup" pass with the file system returned by +/// [createFileSystem], and again as the "test" pass with the file system +/// returned by [replay]. This is intended for use with `ReplayFileSystem`, +/// where in order for the file system to behave as expected, a recording of +/// the invocation(s) must first be made. +void runCommonTests( + FileSystemGenerator createFileSystem, { + RootPathGenerator? root, + List skip = const [], + FileSystemGenerator? replay, +}) { + RootPathGenerator? rootfn = root; + + group('common', () { + late FileSystemGenerator createFs; + late List setUps; + late List tearDowns; + late FileSystem fs; + late String root; + List stack = []; + + void skipIfNecessary(String description, void Function() callback) { + stack.add(description); + bool matchesCurrentFrame(String input) => + RegExp('^$input\$').hasMatch(stack.join(' > ')); + if (skip.where(matchesCurrentFrame).isEmpty) { + callback(); + } + stack.removeLast(); + } + + testpkg.setUp(() async { + createFs = createFileSystem; + setUps = []; + tearDowns = []; + }); + + void setUp(FutureOr Function() callback) { + testpkg.setUp(replay == null ? callback : () => setUps.add(callback)); + } + + void tearDown(FutureOr Function() callback) { + if (replay == null) { + testpkg.tearDown(callback); + } else { + testpkg.setUp(() => tearDowns.insert(0, callback)); + } + } + + void group(String description, void Function() body) => + skipIfNecessary(description, () => testpkg.group(description, body)); + + void test(String description, FutureOr Function() body, + {dynamic skip}) => + skipIfNecessary(description, () { + if (replay == null) { + testpkg.test(description, body, skip: skip); + } else { + group('rerun', () { + testpkg.setUp(() async { + await Future.forEach(setUps, (SetUpTearDown setUp) => setUp()); + await body(); + for (SetUpTearDown tearDown in tearDowns) { + await tearDown(); + } + createFs = replay; + await Future.forEach(setUps, (SetUpTearDown setUp) => setUp()); + }); + + testpkg.test(description, body, skip: skip); + + testpkg.tearDown(() async { + for (SetUpTearDown tearDown in tearDowns) { + await tearDown(); + } + }); + }); + } + }); + + /// Returns [path] prefixed by the [root] namespace. + /// This is only intended for absolute paths. + String ns(String path) { + p.Context posix = p.Context(style: p.Style.posix); + List parts = posix.split(path); + parts[0] = root; + path = fs.path.joinAll(parts); + String rootPrefix = fs.path.rootPrefix(path); + assert(rootPrefix.isNotEmpty); + String result = root == rootPrefix + ? path + : (path == rootPrefix + ? root + : fs.path.join(root, fs.path.joinAll(parts.sublist(1)))); + return result; + } + + setUp(() async { + root = rootfn != null ? rootfn() : '/'; + fs = await createFs() as FileSystem; + assert(fs.path.isAbsolute(root)); + assert(!root.endsWith(fs.path.separator) || + fs.path.rootPrefix(root) == root); + }); + + group('FileSystem', () { + group('directory', () { + test('allowsStringArgument', () { + expect(fs.directory(ns('/foo')), isDirectory); + }); + + test('allowsUriArgument', () { + expect(fs.directory(Uri.parse('file:///')), isDirectory); + }); + + test('succeedsWithUriArgument', () { + fs.directory(ns('/foo')).createSync(); + Uri uri = fs.path.toUri(ns('/foo')); + expect(fs.directory(uri), exists); + }); + + test('allowsDirectoryArgument', () { + expect(fs.directory(io.Directory(ns('/foo'))), isDirectory); + }); + + test('disallowsOtherArgumentType', () { + expect(() => fs.directory(123), throwsArgumentError); + }); + + // Fails due to + // https://github.com/google/file.dart/issues/112 + test('considersBothSlashesEquivalent', () { + fs.directory(r'foo\bar_dir').createSync(recursive: true); + expect(fs.directory(r'foo/bar_dir'), exists); + }, skip: 'Fails due to https://github.com/google/file.dart/issues/112'); + }); + + group('file', () { + test('allowsStringArgument', () { + expect(fs.file(ns('/foo')), isFile); + }); + + test('allowsUriArgument', () { + expect(fs.file(Uri.parse('file:///')), isFile); + }); + + test('succeedsWithUriArgument', () { + fs.file(ns('/foo')).createSync(); + Uri uri = fs.path.toUri(ns('/foo')); + expect(fs.file(uri), exists); + }); + + test('allowsDirectoryArgument', () { + expect(fs.file(io.File(ns('/foo'))), isFile); + }); + + test('disallowsOtherArgumentType', () { + expect(() => fs.file(123), throwsArgumentError); + }); + + // Fails due to + // https://github.com/google/file.dart/issues/112 + test('considersBothSlashesEquivalent', () { + fs.file(r'foo\bar_file').createSync(recursive: true); + expect(fs.file(r'foo/bar_file'), exists); + }, skip: 'Fails due to https://github.com/google/file.dart/issues/112'); + }); + + group('link', () { + test('allowsStringArgument', () { + expect(fs.link(ns('/foo')), isLink); + }); + + test('allowsUriArgument', () { + expect(fs.link(Uri.parse('file:///')), isLink); + }); + + test('succeedsWithUriArgument', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + Uri uri = fs.path.toUri(ns('/bar')); + expect(fs.link(uri), exists); + }); + + test('allowsDirectoryArgument', () { + expect(fs.link(io.File(ns('/foo'))), isLink); + }); + + test('disallowsOtherArgumentType', () { + expect(() => fs.link(123), throwsArgumentError); + }); + }); + + group('path', () { + test('hasCorrectCurrentWorkingDirectory', () { + expect(fs.path.current, fs.currentDirectory.path); + }); + + test('separatorIsAmongExpectedValues', () { + expect(fs.path.separator, anyOf('/', r'\')); + }); + }); + + group('systemTempDirectory', () { + test('existsAsDirectory', () { + Directory tmp = fs.systemTempDirectory; + expect(tmp, isDirectory); + expect(tmp, exists); + }); + }); + + group('currentDirectory', () { + test('defaultsToRoot', () { + expect(fs.currentDirectory.path, root); + }); + + test('throwsIfSetToNonExistentPath', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.currentDirectory = ns('/foo'); + }); + }); + + test('throwsIfHasNonExistentPathInComplexChain', () { + fs.directory(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.currentDirectory = ns('/bar/../foo'); + }); + }); + + test('succeedsIfSetToValidStringPath', () { + fs.directory(ns('/foo')).createSync(); + fs.currentDirectory = ns('/foo'); + expect(fs.currentDirectory.path, ns('/foo')); + }); + + test('succeedsIfSetToValidDirectory', () { + fs.directory(ns('/foo')).createSync(); + fs.currentDirectory = io.Directory(ns('/foo')); + expect(fs.currentDirectory.path, ns('/foo')); + }); + + test('throwsIfArgumentIsNotStringOrDirectory', () { + expect(() { + fs.currentDirectory = 123; + }, throwsArgumentError); + }); + + test('succeedsIfSetToRelativePath', () { + fs.directory(ns('/foo/bar')).createSync(recursive: true); + fs.currentDirectory = 'foo'; + expect(fs.currentDirectory.path, ns('/foo')); + fs.currentDirectory = 'bar'; + expect(fs.currentDirectory.path, ns('/foo/bar')); + }); + + test('succeedsIfSetToAbsolutePathWhenCwdIsNotRoot', () { + fs.directory(ns('/foo/bar')).createSync(recursive: true); + fs.directory(ns('/baz/qux')).createSync(recursive: true); + fs.currentDirectory = ns('/foo/bar'); + expect(fs.currentDirectory.path, ns('/foo/bar')); + fs.currentDirectory = fs.directory(ns('/baz/qux')); + expect(fs.currentDirectory.path, ns('/baz/qux')); + }); + + test('succeedsIfSetToParentDirectory', () { + fs.directory(ns('/foo')).createSync(); + fs.currentDirectory = 'foo'; + expect(fs.currentDirectory.path, ns('/foo')); + fs.currentDirectory = '..'; + expect(fs.currentDirectory.path, ns('/')); + }); + + test('staysAtRootIfSetToParentOfRoot', () { + fs.currentDirectory = + List.filled(20, '..').join(fs.path.separator); + String cwd = fs.currentDirectory.path; + expect(cwd, fs.path.rootPrefix(cwd)); + }); + + test('removesTrailingSlashIfSet', () { + fs.directory(ns('/foo')).createSync(); + fs.currentDirectory = ns('/foo/'); + expect(fs.currentDirectory.path, ns('/foo')); + }); + + test('throwsIfSetToFilePathSegmentAtTail', () { + fs.file(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.ENOTDIR, () { + fs.currentDirectory = ns('/foo'); + }); + }); + + test('throwsIfSetToFilePathSegmentViaTraversal', () { + fs.file(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.ENOTDIR, () { + fs.currentDirectory = ns('/foo/bar/baz'); + }); + }); + + test('resolvesLinksIfEncountered', () { + fs.link(ns('/foo/bar/baz')).createSync(ns('/qux'), recursive: true); + fs.directory(ns('/qux')).createSync(); + fs.directory(ns('/quux')).createSync(); + fs.currentDirectory = ns('/foo/bar/baz/../quux/'); + expect(fs.currentDirectory.path, ns('/quux')); + }); + + test('succeedsIfSetToDirectoryLinkAtTail', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + fs.currentDirectory = ns('/bar'); + expect(fs.currentDirectory.path, ns('/foo')); + }); + + test('throwsIfSetToLinkLoop', () { + fs.link(ns('/foo')).createSync(ns('/bar')); + fs.link(ns('/bar')).createSync(ns('/foo')); + expectFileSystemException( + anyOf(ErrorCodes.EMLINK, ErrorCodes.ELOOP), + () { + fs.currentDirectory = ns('/foo'); + }, + ); + }); + }); + + group('stat', () { + test('isNotFoundForEmptyPath', () { + FileStat stat = fs.statSync(''); + expect(stat.type, FileSystemEntityType.notFound); + }); + + test('isNotFoundForPathToNonExistentEntityAtTail', () { + FileStat stat = fs.statSync(ns('/foo')); + expect(stat.type, FileSystemEntityType.notFound); + }); + + test('isNotFoundForPathToNonExistentEntityInTraversal', () { + FileStat stat = fs.statSync(ns('/foo/bar')); + expect(stat.type, FileSystemEntityType.notFound); + }); + + test('isDirectoryForDirectory', () { + fs.directory(ns('/foo')).createSync(); + FileStat stat = fs.statSync(ns('/foo')); + expect(stat.type, FileSystemEntityType.directory); + }); + + test('isFileForFile', () { + fs.file(ns('/foo')).createSync(); + FileStat stat = fs.statSync(ns('/foo')); + expect(stat.type, FileSystemEntityType.file); + }); + + test('isFileForLinkToFile', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + FileStat stat = fs.statSync(ns('/bar')); + expect(stat.type, FileSystemEntityType.file); + }); + + test('isNotFoundForLinkWithCircularReference', () { + fs.link(ns('/foo')).createSync(ns('/bar')); + fs.link(ns('/bar')).createSync(ns('/baz')); + fs.link(ns('/baz')).createSync(ns('/foo')); + FileStat stat = fs.statSync(ns('/foo')); + expect(stat.type, FileSystemEntityType.notFound); + }); + }); + + group('identical', () { + test('isTrueForIdenticalPathsToExistentFile', () { + fs.file(ns('/foo')).createSync(); + expect(fs.identicalSync(ns('/foo'), ns('/foo')), true); + }); + + test('isFalseForDifferentPathsToDifferentFiles', () { + fs.file(ns('/foo')).createSync(); + fs.file(ns('/bar')).createSync(); + expect(fs.identicalSync(ns('/foo'), ns('/bar')), false); + }); + + test('isTrueForDifferentPathsToSameFileViaLinkInTraversal', () { + fs.file(ns('/foo/file')).createSync(recursive: true); + fs.link(ns('/bar')).createSync(ns('/foo')); + expect(fs.identicalSync(ns('/foo/file'), ns('/bar/file')), true); + }); + + test('isFalseForDifferentPathsToSameFileViaLinkAtTail', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expect(fs.identicalSync(ns('/foo'), ns('/bar')), false); + }); + + test('throwsForDifferentPathsToNonExistentEntities', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.identicalSync(ns('/foo'), ns('/bar')); + }); + }); + + test('throwsForDifferentPathsToOneFileOneNonExistentEntity', () { + fs.file(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.identicalSync(ns('/foo'), ns('/bar')); + }); + }); + }); + + group('type', () { + test('isFileForFile', () { + fs.file(ns('/foo')).createSync(); + FileSystemEntityType type = fs.typeSync(ns('/foo')); + expect(type, FileSystemEntityType.file); + }); + + test('isDirectoryForDirectory', () { + fs.directory(ns('/foo')).createSync(); + FileSystemEntityType type = fs.typeSync(ns('/foo')); + expect(type, FileSystemEntityType.directory); + }); + + test('isDirectoryForAncestorOfRoot', () { + FileSystemEntityType type = fs + .typeSync(List.filled(20, '..').join(fs.path.separator)); + expect(type, FileSystemEntityType.directory); + }); + + test('isFileForLinkToFileAndFollowLinksTrue', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + FileSystemEntityType type = fs.typeSync(ns('/bar')); + expect(type, FileSystemEntityType.file); + }); + + test('isLinkForLinkToFileAndFollowLinksFalse', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + FileSystemEntityType type = + fs.typeSync(ns('/bar'), followLinks: false); + expect(type, FileSystemEntityType.link); + }); + + test('isNotFoundForLinkWithCircularReferenceAndFollowLinksTrue', () { + fs.link(ns('/foo')).createSync(ns('/bar')); + fs.link(ns('/bar')).createSync(ns('/baz')); + fs.link(ns('/baz')).createSync(ns('/foo')); + FileSystemEntityType type = fs.typeSync(ns('/foo')); + expect(type, FileSystemEntityType.notFound); + }); + + test('isNotFoundForNoEntityAtTail', () { + FileSystemEntityType type = fs.typeSync(ns('/foo')); + expect(type, FileSystemEntityType.notFound); + }); + + test('isNotFoundForNoDirectoryInTraversal', () { + FileSystemEntityType type = fs.typeSync(ns('/foo/bar/baz')); + expect(type, FileSystemEntityType.notFound); + }); + }); + + group('isFile/isDirectory/isLink', () { + late String filePath; + late String directoryPath; + late String fileLinkPath; + late String directoryLinkPath; + + setUp(() { + filePath = ns('/file'); + directoryPath = ns('/directory'); + fileLinkPath = ns('/file-link'); + directoryLinkPath = ns('/directory-link'); + + fs.file(filePath).createSync(); + fs.directory(directoryPath).createSync(); + fs.link(fileLinkPath).createSync(filePath); + fs.link(directoryLinkPath).createSync(directoryPath); + }); + + test('isFile', () { + expect(fs.isFileSync(filePath), true); + expect(fs.isFileSync(directoryPath), false); + expect(fs.isFileSync(fileLinkPath), true); + expect(fs.isFileSync(directoryLinkPath), false); + }); + + test('isDirectory', () { + expect(fs.isDirectorySync(filePath), false); + expect(fs.isDirectorySync(directoryPath), true); + expect(fs.isDirectorySync(fileLinkPath), false); + expect(fs.isDirectorySync(directoryLinkPath), true); + }); + + test('isLink', () { + expect(fs.isLinkSync(filePath), false); + expect(fs.isLinkSync(directoryPath), false); + expect(fs.isLinkSync(fileLinkPath), true); + expect(fs.isLinkSync(directoryLinkPath), true); + }); + }); + }); + + group('Directory', () { + test('uri', () { + expect(fs.directory(ns('/foo')).uri, fs.path.toUri('${ns('/foo')}/')); + expect(fs.directory('foo').uri.toString(), 'foo/'); + }); + + group('exists', () { + test('falseIfNotExists', () { + expect(fs.directory(ns('/foo')), isNot(exists)); + expect(fs.directory('foo'), isNot(exists)); + expect(fs.directory(ns('/foo/bar')), isNot(exists)); + }); + + test('trueIfExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + expect(fs.directory(ns('/foo')), exists); + expect(fs.directory('foo'), exists); + }); + + test('falseIfExistsAsFile', () { + fs.file(ns('/foo')).createSync(); + expect(fs.directory(ns('/foo')), isNot(exists)); + expect(fs.directory('foo'), isNot(exists)); + }); + + test('trueIfExistsAsLinkToDirectory', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expect(fs.directory(ns('/bar')), exists); + expect(fs.directory('bar'), exists); + }); + + test('falseIfExistsAsLinkToFile', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expect(fs.directory(ns('/bar')), isNot(exists)); + expect(fs.directory('bar'), isNot(exists)); + }); + + test('falseIfNotFoundSegmentExistsThenIsBackedOut', () { + fs.directory(ns('/foo')).createSync(); + expect(fs.directory(ns('/bar/../foo')), isNot(exists)); + }); + }); + + group('create', () { + test('returnsCovariantType', () async { + expect(await fs.directory(ns('/foo')).create(), isDirectory); + }); + + test('succeedsIfAlreadyExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + fs.directory(ns('/foo')).createSync(); + }); + + test('throwsIfAlreadyExistsAsFile', () { + fs.file(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.ENOTDIR, () { + fs.directory(ns('/foo')).createSync(); + }); + }); + + test('succeedsIfAlreadyExistsAsLinkToDirectory', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + fs.directory(ns('/bar')).createSync(); + }); + + test('throwsIfAlreadyExistsAsLinkToFile', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + // TODO(tvolkert): Change this to just be 'Not a directory' + // once Dart 1.22 is stable. + expectFileSystemException( + anyOf(ErrorCodes.EEXIST, ErrorCodes.ENOTDIR), + () { + fs.directory(ns('/bar')).createSync(); + }, + ); + }); + + test('throwsIfAlreadyExistsAsLinkToNotFoundAtTail', () { + fs.link(ns('/foo')).createSync(ns('/bar')); + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.directory(ns('/foo')).createSync(); + }); + }); + + test('throwsIfAlreadyExistsAsLinkToNotFoundViaTraversal', () { + fs.link(ns('/foo')).createSync(ns('/bar/baz')); + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.directory(ns('/foo')).createSync(); + }); + }); + + test('throwsIfAlreadyExistsAsLinkToNotFoundInDifferentDirectory', () { + fs.directory(ns('/foo')).createSync(); + fs.directory(ns('/bar')).createSync(); + fs.link(ns('/bar/baz')).createSync(ns('/foo/qux')); + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.directory(ns('/bar/baz')).createSync(); + }); + }); + + test('succeedsIfTailDoesntExist', () { + expect(fs.directory(ns('/')), exists); + fs.directory(ns('/foo')).createSync(); + expect(fs.directory(ns('/foo')), exists); + }); + + test('throwsIfAncestorDoesntExistRecursiveFalse', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.directory(ns('/foo/bar')).createSync(); + }); + }); + + test('succeedsIfAncestorDoesntExistRecursiveTrue', () { + fs.directory(ns('/foo/bar')).createSync(recursive: true); + expect(fs.directory(ns('/foo')), exists); + expect(fs.directory(ns('/foo/bar')), exists); + }); + }); + + group('rename', () { + test('returnsCovariantType', () async { + Directory src() => fs.directory(ns('/foo'))..createSync(); + expect(src().renameSync(ns('/bar')), isDirectory); + expect(await src().rename(ns('/baz')), isDirectory); + }); + + test('succeedsIfDestinationDoesntExist', () { + Directory src = fs.directory(ns('/foo'))..createSync(); + Directory dest = src.renameSync(ns('/bar')); + expect(dest.path, ns('/bar')); + expect(dest, exists); + }); + + test( + 'succeedsIfDestinationIsEmptyDirectory', + () { + fs.directory(ns('/bar')).createSync(); + Directory src = fs.directory(ns('/foo'))..createSync(); + Directory dest = src.renameSync(ns('/bar')); + expect(src, isNot(exists)); + expect(dest, exists); + }, + // See https://github.com/google/file.dart/issues/197. + skip: io.Platform.isWindows, + ); + + test('throwsIfDestinationIsFile', () { + fs.file(ns('/bar')).createSync(); + Directory src = fs.directory(ns('/foo'))..createSync(); + expectFileSystemException(ErrorCodes.ENOTDIR, () { + src.renameSync(ns('/bar')); + }); + }); + + test('throwsIfDestinationParentFolderDoesntExist', () { + Directory src = fs.directory(ns('/foo'))..createSync(); + expectFileSystemException(ErrorCodes.ENOENT, () { + src.renameSync(ns('/bar/baz')); + }); + }); + + test('throwsIfDestinationIsNonEmptyDirectory', () { + fs.file(ns('/bar/baz')).createSync(recursive: true); + Directory src = fs.directory(ns('/foo'))..createSync(); + // The error will be 'Directory not empty' on OS X, but it will be + // 'File exists' on Linux. + expectFileSystemException( + anyOf(ErrorCodes.ENOTEMPTY, ErrorCodes.EEXIST), + () { + src.renameSync(ns('/bar')); + }, + ); + }); + + test('throwsIfSourceDoesntExist', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.directory(ns('/foo')).renameSync(ns('/bar')); + }); + }); + + test('throwsIfSourceIsFile', () { + fs.file(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.ENOTDIR, () { + fs.directory(ns('/foo')).renameSync(ns('/bar')); + }); + }); + + test('succeedsIfSourceIsLinkToDirectory', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + fs.directory(ns('/bar')).renameSync(ns('/baz')); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.directory); + expect(fs.typeSync(ns('/bar')), FileSystemEntityType.notFound); + expect(fs.typeSync(ns('/baz'), followLinks: false), + FileSystemEntityType.link); + expect(fs.link(ns('/baz')).targetSync(), ns('/foo')); + }); + + test('throwsIfDestinationIsLinkToNotFound', () { + Directory src = fs.directory(ns('/foo'))..createSync(); + fs.link(ns('/bar')).createSync(ns('/baz')); + expectFileSystemException(ErrorCodes.ENOTDIR, () { + src.renameSync(ns('/bar')); + }); + }); + + test('throwsIfDestinationIsLinkToEmptyDirectory', () { + Directory src = fs.directory(ns('/foo'))..createSync(); + fs.directory(ns('/bar')).createSync(); + fs.link(ns('/baz')).createSync(ns('/bar')); + expectFileSystemException(ErrorCodes.ENOTDIR, () { + src.renameSync(ns('/baz')); + }); + }); + + test('succeedsIfDestinationIsInDifferentDirectory', () { + Directory src = fs.directory(ns('/foo'))..createSync(); + fs.directory(ns('/bar')).createSync(); + src.renameSync(ns('/bar/baz')); + expect(fs.typeSync(ns('/foo')), FileSystemEntityType.notFound); + expect(fs.typeSync(ns('/bar/baz')), FileSystemEntityType.directory); + }); + + test('succeedsIfSourceIsLinkToDifferentDirectory', () { + fs.directory(ns('/foo/subfoo')).createSync(recursive: true); + fs.directory(ns('/bar/subbar')).createSync(recursive: true); + fs.directory(ns('/baz/subbaz')).createSync(recursive: true); + fs.link(ns('/foo/subfoo/lnk')).createSync(ns('/bar/subbar')); + fs.directory(ns('/foo/subfoo/lnk')).renameSync(ns('/baz/subbaz/dst')); + expect(fs.typeSync(ns('/foo/subfoo/lnk')), + FileSystemEntityType.notFound); + expect(fs.typeSync(ns('/baz/subbaz/dst'), followLinks: false), + FileSystemEntityType.link); + expect(fs.typeSync(ns('/baz/subbaz/dst'), followLinks: true), + FileSystemEntityType.directory); + }); + }); + + group('delete', () { + test('returnsCovariantType', () async { + Directory dir = fs.directory(ns('/foo'))..createSync(); + expect(await dir.delete(), isDirectory); + }); + + test('succeedsIfEmptyDirectoryExistsAndRecursiveFalse', () { + Directory dir = fs.directory(ns('/foo'))..createSync(); + dir.deleteSync(); + expect(dir, isNot(exists)); + }); + + test('succeedsIfEmptyDirectoryExistsAndRecursiveTrue', () { + Directory dir = fs.directory(ns('/foo'))..createSync(); + dir.deleteSync(recursive: true); + expect(dir, isNot(exists)); + }); + + test('throwsIfNonEmptyDirectoryExistsAndRecursiveFalse', () { + Directory dir = fs.directory(ns('/foo'))..createSync(); + fs.file(ns('/foo/bar')).createSync(); + expectFileSystemException(ErrorCodes.ENOTEMPTY, () { + dir.deleteSync(); + }); + }); + + test('succeedsIfNonEmptyDirectoryExistsAndRecursiveTrue', () { + Directory dir = fs.directory(ns('/foo'))..createSync(); + fs.file(ns('/foo/bar')).createSync(); + dir.deleteSync(recursive: true); + expect(fs.directory(ns('/foo')), isNot(exists)); + expect(fs.file(ns('/foo/bar')), isNot(exists)); + }); + + test('throwsIfDirectoryDoesntExistAndRecursiveFalse', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.directory(ns('/foo')).deleteSync(); + }); + }); + + test('throwsIfDirectoryDoesntExistAndRecursiveTrue', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.directory(ns('/foo')).deleteSync(recursive: true); + }); + }); + + test('succeedsIfPathReferencesFileAndRecursiveTrue', () { + fs.file(ns('/foo')).createSync(); + fs.directory(ns('/foo')).deleteSync(recursive: true); + expect(fs.typeSync(ns('/foo')), FileSystemEntityType.notFound); + }); + + test('throwsIfPathReferencesFileAndRecursiveFalse', () { + fs.file(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.ENOTDIR, () { + fs.directory(ns('/foo')).deleteSync(); + }); + }); + + test('succeedsIfPathReferencesLinkToDirectoryAndRecursiveTrue', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + fs.directory(ns('/bar')).deleteSync(recursive: true); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.directory); + expect(fs.typeSync(ns('/bar'), followLinks: false), + FileSystemEntityType.notFound); + }); + + test('succeedsIfPathReferencesLinkToDirectoryAndRecursiveFalse', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + fs.directory(ns('/bar')).deleteSync(); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.directory); + expect(fs.typeSync(ns('/bar'), followLinks: false), + FileSystemEntityType.notFound); + }); + + test('succeedsIfExistsAsLinkToDirectoryInDifferentDirectory', () { + fs.directory(ns('/foo/bar')).createSync(recursive: true); + fs.link(ns('/baz/qux')).createSync(ns('/foo/bar'), recursive: true); + fs.directory(ns('/baz/qux')).deleteSync(); + expect(fs.typeSync(ns('/foo/bar'), followLinks: false), + FileSystemEntityType.directory); + expect(fs.typeSync(ns('/baz/qux'), followLinks: false), + FileSystemEntityType.notFound); + }); + + test('succeedsIfPathReferencesLinkToFileAndRecursiveTrue', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + fs.directory(ns('/bar')).deleteSync(recursive: true); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.file); + expect(fs.typeSync(ns('/bar'), followLinks: false), + FileSystemEntityType.notFound); + }); + + test('throwsIfPathReferencesLinkToFileAndRecursiveFalse', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expectFileSystemException(ErrorCodes.ENOTDIR, () { + fs.directory(ns('/bar')).deleteSync(); + }); + }); + + test('throwsIfPathReferencesLinkToNotFoundAndRecursiveFalse', () { + fs.link(ns('/foo')).createSync(ns('/bar')); + expectFileSystemException(ErrorCodes.ENOTDIR, () { + fs.directory(ns('/foo')).deleteSync(); + }); + }); + }); + + group('resolveSymbolicLinks', () { + test('succeedsForRootDirectory', () { + expect(fs.directory(ns('/')).resolveSymbolicLinksSync(), ns('/')); + }); + + test('throwsIfPathIsEmpty', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.directory('').resolveSymbolicLinksSync(); + }); + }); + + test('throwsIfLoopInLinkChain', () { + fs.link(ns('/foo')).createSync(ns('/bar')); + fs.link(ns('/bar')).createSync(ns('/baz')); + fs.link(ns('/baz')).createSync(ns('/foo')); + expectFileSystemException( + anyOf(ErrorCodes.EMLINK, ErrorCodes.ELOOP), + () { + fs.directory(ns('/foo')).resolveSymbolicLinksSync(); + }, + ); + }); + + test('throwsIfPathNotFoundInTraversal', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.directory(ns('/foo/bar')).resolveSymbolicLinksSync(); + }); + }); + + test('throwsIfPathNotFoundAtTail', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.directory(ns('/foo')).resolveSymbolicLinksSync(); + }); + }); + + test('throwsIfPathNotFoundInMiddleThenBackedOut', () { + fs.directory(ns('/foo/bar')).createSync(recursive: true); + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.directory(ns('/foo/baz/../bar')).resolveSymbolicLinksSync(); + }); + }); + + test('resolvesRelativePathToCurrentDirectory', () { + fs.directory(ns('/foo/bar')).createSync(recursive: true); + fs.link(ns('/foo/baz')).createSync(ns('/foo/bar')); + fs.currentDirectory = ns('/foo'); + expect( + fs.directory('baz').resolveSymbolicLinksSync(), ns('/foo/bar')); + }); + + test('resolvesAbsolutePathsAbsolutely', () { + fs.directory(ns('/foo/bar')).createSync(recursive: true); + fs.currentDirectory = ns('/foo'); + expect(fs.directory(ns('/foo/bar')).resolveSymbolicLinksSync(), + ns('/foo/bar')); + }); + + test('handlesRelativeLinks', () { + fs.directory(ns('/foo/bar/baz')).createSync(recursive: true); + fs.link(ns('/foo/qux')).createSync(fs.path.join('bar', 'baz')); + expect( + fs.directory(ns('/foo/qux')).resolveSymbolicLinksSync(), + ns('/foo/bar/baz'), + ); + expect( + fs.directory(fs.path.join('foo', 'qux')).resolveSymbolicLinksSync(), + ns('/foo/bar/baz'), + ); + }); + + test('handlesAbsoluteLinks', () { + fs.directory(ns('/foo')).createSync(); + fs.directory(ns('/bar/baz/qux')).createSync(recursive: true); + fs.link(ns('/foo/quux')).createSync(ns('/bar/baz/qux')); + expect(fs.directory(ns('/foo/quux')).resolveSymbolicLinksSync(), + ns('/bar/baz/qux')); + }); + + test('handlesLinksWhoseTargetsHaveNestedLinks', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/foo/quuz')).createSync(ns('/bar')); + fs.link(ns('/foo/grault')).createSync(ns('/baz/quux')); + fs.directory(ns('/bar')).createSync(); + fs.link(ns('/bar/qux')).createSync(ns('/baz')); + fs.link(ns('/bar/garply')).createSync(ns('/foo')); + fs.directory(ns('/baz')).createSync(); + fs.link(ns('/baz/quux')).createSync(ns('/bar/garply/quuz')); + expect(fs.directory(ns('/foo/grault/qux')).resolveSymbolicLinksSync(), + ns('/baz')); + }); + + test('handlesParentAndThisFolderReferences', () { + fs.directory(ns('/foo/bar/baz')).createSync(recursive: true); + fs.link(ns('/foo/bar/baz/qux')).createSync(fs.path.join('..', '..')); + String resolved = fs + .directory(ns('/foo/./bar/baz/../baz/qux/bar')) + .resolveSymbolicLinksSync(); + expect(resolved, ns('/foo/bar')); + }); + + test('handlesBackToBackSlashesInPath', () { + fs.directory(ns('/foo/bar/baz')).createSync(recursive: true); + expect(fs.directory(ns('//foo/bar///baz')).resolveSymbolicLinksSync(), + ns('/foo/bar/baz')); + }); + + test('handlesComplexPathWithMultipleLinks', () { + fs + .link(ns('/foo/bar/baz')) + .createSync(fs.path.join('..', '..', 'qux'), recursive: true); + fs.link(ns('/qux')).createSync('quux'); + fs.link(ns('/quux/quuz')).createSync(ns('/foo'), recursive: true); + String resolved = fs + .directory(ns('/foo//bar/./baz/quuz/bar/..///bar/baz/')) + .resolveSymbolicLinksSync(); + expect(resolved, ns('/quux')); + }); + }); + + group('absolute', () { + test('returnsCovariantType', () { + expect(fs.directory('foo').absolute, isDirectory); + }); + + test('returnsSamePathIfAlreadyAbsolute', () { + expect(fs.directory(ns('/foo')).absolute.path, ns('/foo')); + }); + + test('succeedsForRelativePaths', () { + expect(fs.directory('foo').absolute.path, ns('/foo')); + }); + }); + + group('parent', () { + late String root; + + setUp(() { + root = fs.path.style.name == 'windows' ? r'C:\' : '/'; + }); + + test('returnsCovariantType', () { + expect(fs.directory(root).parent, isDirectory); + }); + + test('returnsRootForRoot', () { + expect(fs.directory(root).parent.path, root); + }); + + test('succeedsForNonRoot', () { + expect(fs.directory(ns('/foo/bar')).parent.path, ns('/foo')); + }); + }); + + group('createTemp', () { + test('returnsCovariantType', () { + expect(fs.directory(ns('/')).createTempSync(), isDirectory); + }); + + test('throwsIfDirectoryDoesntExist', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.directory(ns('/foo')).createTempSync(); + }); + }); + + test('resolvesNameCollisions', () { + fs.directory(ns('/foo/bar')).createSync(recursive: true); + Directory tmp = fs.directory(ns('/foo')).createTempSync('bar'); + expect(tmp.path, + allOf(isNot(ns('/foo/bar')), startsWith(ns('/foo/bar')))); + }); + + test('succeedsWithoutPrefix', () { + Directory dir = fs.directory(ns('/foo'))..createSync(); + expect(dir.createTempSync().path, startsWith(ns('/foo/'))); + }); + + test('succeedsWithPrefix', () { + Directory dir = fs.directory(ns('/foo'))..createSync(); + expect(dir.createTempSync('bar').path, startsWith(ns('/foo/bar'))); + }); + + test('succeedsWithNestedPathPrefixThatExists', () { + fs.directory(ns('/foo/bar')).createSync(recursive: true); + Directory tmp = fs.directory(ns('/foo')).createTempSync('bar/baz'); + expect(tmp.path, startsWith(ns('/foo/bar/baz'))); + }); + + test('throwsWithNestedPathPrefixThatDoesntExist', () { + Directory dir = fs.directory(ns('/foo'))..createSync(); + expectFileSystemException(ErrorCodes.ENOENT, () { + dir.createTempSync('bar/baz'); + }); + }); + }); + + group('list', () { + late Directory dir; + + setUp(() { + dir = fs.currentDirectory = fs.directory(ns('/foo'))..createSync(); + fs.file('bar').createSync(); + fs.file(fs.path.join('baz', 'qux')).createSync(recursive: true); + fs.link('quux').createSync(fs.path.join('baz', 'qux')); + fs + .link(fs.path.join('baz', 'quuz')) + .createSync(fs.path.join('..', 'quux')); + fs.link(fs.path.join('baz', 'grault')).createSync('.'); + fs.currentDirectory = ns('/'); + }); + + test('returnsCovariantType', () async { + void expectIsFileSystemEntity(dynamic entity) { + expect(entity, isFileSystemEntity); + } + + dir.listSync().forEach(expectIsFileSystemEntity); + (await dir.list().toList()).forEach(expectIsFileSystemEntity); + }); + + test('returnsEmptyListForEmptyDirectory', () { + Directory empty = fs.directory(ns('/bar'))..createSync(); + expect(empty.listSync(), isEmpty); + }); + + test('throwsIfDirectoryDoesntExist', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.directory(ns('/bar')).listSync(); + }); + }); + + test('returnsLinkObjectsIfFollowLinksFalse', () { + List list = dir.listSync(followLinks: false); + expect(list, hasLength(3)); + expect(list, contains(allOf(isFile, hasPath(ns('/foo/bar'))))); + expect(list, contains(allOf(isDirectory, hasPath(ns('/foo/baz'))))); + expect(list, contains(allOf(isLink, hasPath(ns('/foo/quux'))))); + }); + + test('followsLinksIfFollowLinksTrue', () { + List list = dir.listSync(); + expect(list, hasLength(3)); + expect(list, contains(allOf(isFile, hasPath(ns('/foo/bar'))))); + expect(list, contains(allOf(isDirectory, hasPath(ns('/foo/baz'))))); + expect(list, contains(allOf(isFile, hasPath(ns('/foo/quux'))))); + }); + + test('returnsLinkObjectsForRecursiveLinkIfFollowLinksTrue', () { + expect( + dir.listSync(recursive: true), + allOf( + hasLength(9), + allOf( + contains(allOf(isFile, hasPath(ns('/foo/bar')))), + contains(allOf(isFile, hasPath(ns('/foo/quux')))), + contains(allOf(isFile, hasPath(ns('/foo/baz/qux')))), + contains(allOf(isFile, hasPath(ns('/foo/baz/quuz')))), + contains(allOf(isFile, hasPath(ns('/foo/baz/grault/qux')))), + contains(allOf(isFile, hasPath(ns('/foo/baz/grault/quuz')))), + ), + allOf( + contains(allOf(isDirectory, hasPath(ns('/foo/baz')))), + contains(allOf(isDirectory, hasPath(ns('/foo/baz/grault')))), + ), + contains(allOf(isLink, hasPath(ns('/foo/baz/grault/grault')))), + ), + ); + }); + + test('recurseIntoDirectoriesIfRecursiveTrueFollowLinksFalse', () { + expect( + dir.listSync(recursive: true, followLinks: false), + allOf( + hasLength(6), + contains(allOf(isFile, hasPath(ns('/foo/bar')))), + contains(allOf(isFile, hasPath(ns('/foo/baz/qux')))), + contains(allOf(isLink, hasPath(ns('/foo/quux')))), + contains(allOf(isLink, hasPath(ns('/foo/baz/quuz')))), + contains(allOf(isLink, hasPath(ns('/foo/baz/grault')))), + contains(allOf(isDirectory, hasPath(ns('/foo/baz')))), + ), + ); + }); + + test('childEntriesNotNormalized', () { + dir = fs.directory(ns('/bar/baz'))..createSync(recursive: true); + fs.file(ns('/bar/baz/qux')).createSync(); + List list = + fs.directory(ns('/bar//../bar/./baz')).listSync(); + expect(list, hasLength(1)); + expect(list[0], allOf(isFile, hasPath(ns('/bar//../bar/./baz/qux')))); + }); + + test('symlinksToNotFoundAlwaysReturnedAsLinks', () { + dir = fs.directory(ns('/bar'))..createSync(); + fs.link(ns('/bar/baz')).createSync('qux'); + for (bool followLinks in const [true, false]) { + List list = + dir.listSync(followLinks: followLinks); + expect(list, hasLength(1)); + expect(list[0], allOf(isLink, hasPath(ns('/bar/baz')))); + } + }); + }); + + test('childEntities', () { + Directory dir = fs.directory(ns('/foo'))..createSync(); + dir.childDirectory('bar').createSync(); + dir.childFile('baz').createSync(); + dir.childLink('qux').createSync('bar'); + expect(fs.directory(ns('/foo/bar')), exists); + expect(fs.file(ns('/foo/baz')), exists); + expect(fs.link(ns('/foo/qux')), exists); + }); + }); + + group('File', () { + test('uri', () { + expect(fs.file(ns('/foo')).uri, fs.path.toUri(ns('/foo'))); + expect(fs.file('foo').uri.toString(), 'foo'); + }); + + group('create', () { + test('returnsCovariantType', () async { + expect(await fs.file(ns('/foo')).create(), isFile); + }); + + test('succeedsIfTailDoesntAlreadyExist', () { + fs.file(ns('/foo')).createSync(); + expect(fs.file(ns('/foo')), exists); + }); + + test('succeedsIfAlreadyExistsAsFile', () { + fs.file(ns('/foo')).createSync(); + fs.file(ns('/foo')).createSync(); + expect(fs.file(ns('/foo')), exists); + }); + + test('throwsIfAncestorDoesntExistRecursiveFalse', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/foo/bar')).createSync(); + }); + }); + + test('succeedsIfAncestorDoesntExistRecursiveTrue', () { + fs.file(ns('/foo/bar')).createSync(recursive: true); + expect(fs.file(ns('/foo/bar')), exists); + }); + + test('throwsIfAlreadyExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/foo')).createSync(); + }); + }); + + test('throwsIfAlreadyExistsAsLinkToDirectory', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/bar')).createSync(); + }); + }); + + test('succeedsIfAlreadyExistsAsLinkToFile', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + fs.file(ns('/bar')).createSync(); + expect(fs.file(ns('/bar')), exists); + }); + + test('succeedsIfAlreadyExistsAsLinkToNotFoundAtTail', () { + fs.link(ns('/foo')).createSync(ns('/bar')); + fs.file(ns('/foo')).createSync(); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.link); + expect(fs.typeSync(ns('/bar'), followLinks: false), + FileSystemEntityType.file); + }); + + test('throwsIfAlreadyExistsAsLinkToNotFoundViaTraversal', () { + fs.link(ns('/foo')).createSync(ns('/bar/baz')); + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/foo')).createSync(); + }); + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/foo')).createSync(recursive: true); + }); + }); + + /* + test('throwsIfPathSegmentIsLinkToNotFoundAndRecursiveTrue', () { + fs.link(ns('/foo')).createSync(ns('/bar')); + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/foo/baz')).createSync(recursive: true); + }); + }); + */ + + test('succeedsIfAlreadyExistsAsLinkToNotFoundInDifferentDirectory', () { + fs.directory(ns('/foo')).createSync(); + fs.directory(ns('/bar')).createSync(); + fs.link(ns('/bar/baz')).createSync(ns('/foo/qux')); + fs.file(ns('/bar/baz')).createSync(); + expect(fs.typeSync(ns('/bar/baz'), followLinks: false), + FileSystemEntityType.link); + expect(fs.typeSync(ns('/foo/qux'), followLinks: false), + FileSystemEntityType.file); + }); + }); + + group('rename', () { + test('returnsCovariantType', () async { + File f() => fs.file(ns('/foo'))..createSync(); + expect(await f().rename(ns('/bar')), isFile); + expect(f().renameSync(ns('/baz')), isFile); + }); + + test('succeedsIfDestinationDoesntExistAtTail', () { + File src = fs.file(ns('/foo'))..createSync(); + File dest = src.renameSync(ns('/bar')); + expect(fs.file(ns('/foo')), isNot(exists)); + expect(fs.file(ns('/bar')), exists); + expect(dest.path, ns('/bar')); + }); + + test('throwsIfDestinationDoesntExistViaTraversal', () { + File f = fs.file(ns('/foo'))..createSync(); + expectFileSystemException(ErrorCodes.ENOENT, () { + f.renameSync(ns('/bar/baz')); + }); + }); + + test('succeedsIfDestinationExistsAsFile', () { + File f = fs.file(ns('/foo'))..createSync(); + fs.file(ns('/bar')).createSync(); + f.renameSync(ns('/bar')); + expect(fs.file(ns('/foo')), isNot(exists)); + expect(fs.file(ns('/bar')), exists); + }); + + test('throwsIfDestinationExistsAsDirectory', () { + File f = fs.file(ns('/foo'))..createSync(); + fs.directory(ns('/bar')).createSync(); + expectFileSystemException(ErrorCodes.EISDIR, () { + f.renameSync(ns('/bar')); + }); + }); + + test('succeedsIfDestinationExistsAsLinkToFile', () { + File f = fs.file(ns('/foo'))..createSync(); + fs.file(ns('/bar')).createSync(); + fs.link(ns('/baz')).createSync(ns('/bar')); + f.renameSync(ns('/baz')); + expect(fs.typeSync(ns('/foo')), FileSystemEntityType.notFound); + expect(fs.typeSync(ns('/bar'), followLinks: false), + FileSystemEntityType.file); + expect(fs.typeSync(ns('/baz'), followLinks: false), + FileSystemEntityType.file); + }); + + test('throwsIfDestinationExistsAsLinkToDirectory', () { + File f = fs.file(ns('/foo'))..createSync(); + fs.directory(ns('/bar')).createSync(); + fs.link(ns('/baz')).createSync(ns('/bar')); + expectFileSystemException(ErrorCodes.EISDIR, () { + f.renameSync(ns('/baz')); + }); + }); + + test('succeedsIfDestinationExistsAsLinkToNotFound', () { + File f = fs.file(ns('/foo'))..createSync(); + fs.link(ns('/bar')).createSync(ns('/baz')); + f.renameSync(ns('/bar')); + expect(fs.typeSync(ns('/foo')), FileSystemEntityType.notFound); + expect(fs.typeSync(ns('/bar'), followLinks: false), + FileSystemEntityType.file); + }); + + test('throwsIfSourceDoesntExist', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/foo')).renameSync(ns('/bar')); + }); + }); + + test('throwsIfSourceExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/foo')).renameSync(ns('/bar')); + }); + }); + + test('succeedsIfSourceExistsAsLinkToFile', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + fs.file(ns('/bar')).renameSync(ns('/baz')); + expect(fs.typeSync(ns('/bar')), FileSystemEntityType.notFound); + expect(fs.typeSync(ns('/baz'), followLinks: false), + FileSystemEntityType.link); + expect(fs.typeSync(ns('/baz'), followLinks: true), + FileSystemEntityType.file); + }); + + test('throwsIfSourceExistsAsLinkToDirectory', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/bar')).renameSync(ns('/baz')); + }); + }); + + test('throwsIfSourceExistsAsLinkToNotFound', () { + fs.link(ns('/foo')).createSync(ns('/bar')); + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/foo')).renameSync(ns('/baz')); + }); + }); + }); + + group('copy', () { + test('returnsCovariantType', () async { + File f() => fs.file(ns('/foo'))..createSync(); + expect(await f().copy(ns('/bar')), isFile); + expect(f().copySync(ns('/baz')), isFile); + }); + + test('succeedsIfDestinationDoesntExistAtTail', () { + File f = fs.file(ns('/foo')) + ..createSync() + ..writeAsStringSync('foo'); + f.copySync(ns('/bar')); + expect(fs.file(ns('/foo')), exists); + expect(fs.file(ns('/bar')), exists); + expect(fs.file(ns('/foo')).readAsStringSync(), 'foo'); + }); + + test('throwsIfDestinationDoesntExistViaTraversal', () { + File f = fs.file(ns('/foo'))..createSync(); + expectFileSystemException(ErrorCodes.ENOENT, () { + f.copySync(ns('/bar/baz')); + }); + }); + + test('succeedsIfDestinationExistsAsFile', () { + File f = fs.file(ns('/foo')) + ..createSync() + ..writeAsStringSync('foo'); + fs.file(ns('/bar')) + ..createSync() + ..writeAsStringSync('bar'); + f.copySync(ns('/bar')); + expect(fs.file(ns('/foo')), exists); + expect(fs.file(ns('/bar')), exists); + expect(fs.file(ns('/foo')).readAsStringSync(), 'foo'); + expect(fs.file(ns('/bar')).readAsStringSync(), 'foo'); + }); + + test('throwsIfDestinationExistsAsDirectory', () { + File f = fs.file(ns('/foo'))..createSync(); + fs.directory(ns('/bar')).createSync(); + expectFileSystemException(ErrorCodes.EISDIR, () { + f.copySync(ns('/bar')); + }); + }); + + test('succeedsIfDestinationExistsAsLinkToFile', () { + File f = fs.file(ns('/foo')) + ..createSync() + ..writeAsStringSync('foo'); + fs.file(ns('/bar')) + ..createSync() + ..writeAsStringSync('bar'); + fs.link(ns('/baz')).createSync(ns('/bar')); + f.copySync(ns('/baz')); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.file); + expect(fs.typeSync(ns('/bar'), followLinks: false), + FileSystemEntityType.file); + expect(fs.typeSync(ns('/baz'), followLinks: false), + FileSystemEntityType.link); + expect(fs.file(ns('/foo')).readAsStringSync(), 'foo'); + expect(fs.file(ns('/bar')).readAsStringSync(), 'foo'); + }, skip: io.Platform.isWindows /* No links on Windows */); + + test('throwsIfDestinationExistsAsLinkToDirectory', () { + File f = fs.file(ns('/foo'))..createSync(); + fs.directory(ns('/bar')).createSync(); + fs.link(ns('/baz')).createSync(ns('/bar')); + expectFileSystemException(ErrorCodes.EISDIR, () { + f.copySync(ns('/baz')); + }); + }); + + test('throwsIfSourceDoesntExist', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/foo')).copySync(ns('/bar')); + }); + }); + + test('throwsIfSourceExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/foo')).copySync(ns('/bar')); + }); + }); + + test('succeedsIfSourceExistsAsLinkToFile', () { + fs.file(ns('/foo')) + ..createSync() + ..writeAsStringSync('foo'); + fs.link(ns('/bar')).createSync(ns('/foo')); + fs.file(ns('/bar')).copySync(ns('/baz')); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.file); + expect(fs.typeSync(ns('/bar'), followLinks: false), + FileSystemEntityType.link); + expect(fs.typeSync(ns('/baz'), followLinks: false), + FileSystemEntityType.file); + expect(fs.file(ns('/foo')).readAsStringSync(), 'foo'); + expect(fs.file(ns('/baz')).readAsStringSync(), 'foo'); + }); + + test('succeedsIfDestinationIsInDifferentDirectoryThanSource', () { + File f = fs.file(ns('/foo/bar')) + ..createSync(recursive: true) + ..writeAsStringSync('foo'); + fs.directory(ns('/baz')).createSync(); + f.copySync(ns('/baz/qux')); + expect(fs.file(ns('/foo/bar')), exists); + expect(fs.file(ns('/baz/qux')), exists); + expect(fs.file(ns('/foo/bar')).readAsStringSync(), 'foo'); + expect(fs.file(ns('/baz/qux')).readAsStringSync(), 'foo'); + }); + + test('succeedsIfSourceIsLinkToFileInDifferentDirectory', () { + fs.file(ns('/foo/bar')) + ..createSync(recursive: true) + ..writeAsStringSync('foo'); + fs.link(ns('/baz/qux')).createSync(ns('/foo/bar'), recursive: true); + fs.file(ns('/baz/qux')).copySync(ns('/baz/quux')); + expect(fs.typeSync(ns('/foo/bar'), followLinks: false), + FileSystemEntityType.file); + expect(fs.typeSync(ns('/baz/qux'), followLinks: false), + FileSystemEntityType.link); + expect(fs.typeSync(ns('/baz/quux'), followLinks: false), + FileSystemEntityType.file); + expect(fs.file(ns('/foo/bar')).readAsStringSync(), 'foo'); + expect(fs.file(ns('/baz/quux')).readAsStringSync(), 'foo'); + }); + + test('succeedsIfDestinationIsLinkToFileInDifferentDirectory', () { + fs.file(ns('/foo/bar')) + ..createSync(recursive: true) + ..writeAsStringSync('bar'); + fs.file(ns('/baz/qux')) + ..createSync(recursive: true) + ..writeAsStringSync('qux'); + fs.link(ns('/baz/quux')).createSync(ns('/foo/bar')); + fs.file(ns('/baz/qux')).copySync(ns('/baz/quux')); + expect(fs.typeSync(ns('/foo/bar'), followLinks: false), + FileSystemEntityType.file); + expect(fs.typeSync(ns('/baz/qux'), followLinks: false), + FileSystemEntityType.file); + expect(fs.typeSync(ns('/baz/quux'), followLinks: false), + FileSystemEntityType.link); + expect(fs.file(ns('/foo/bar')).readAsStringSync(), 'qux'); + expect(fs.file(ns('/baz/qux')).readAsStringSync(), 'qux'); + }, skip: io.Platform.isWindows /* No links on Windows */); + }); + + group('length', () { + test('throwsIfDoesntExist', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/foo')).lengthSync(); + }); + }); + + test('throwsIfExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/foo')).lengthSync(); + }); + }); + + test('returnsZeroForNewlyCreatedFile', () { + File f = fs.file(ns('/foo'))..createSync(); + expect(f.lengthSync(), 0); + }); + + test('writeNBytesReturnsLengthN', () { + File f = fs.file(ns('/foo'))..createSync(); + f.writeAsBytesSync([1, 2, 3, 4], flush: true); + expect(f.lengthSync(), 4); + }); + + test('succeedsIfExistsAsLinkToFile', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expect(fs.file(ns('/bar')).lengthSync(), 0); + }); + }); + + group('absolute', () { + test('returnsSamePathIfAlreadyAbsolute', () { + expect(fs.file(ns('/foo')).absolute.path, ns('/foo')); + }); + + test('succeedsForRelativePaths', () { + expect(fs.file('foo').absolute.path, ns('/foo')); + }); + }); + + group('lastAccessed', () { + test('isNowForNewlyCreatedFile', () { + DateTime before = downstairs(); + File f = fs.file(ns('/foo'))..createSync(); + DateTime after = ceil(); + DateTime accessed = f.lastAccessedSync(); + expect(accessed, isSameOrAfter(before)); + expect(accessed, isSameOrBefore(after)); + }); + + test('throwsIfDoesntExist', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/foo')).lastAccessedSync(); + }); + }); + + test('throwsIfExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/foo')).lastAccessedSync(); + }); + }); + + test('succeedsIfExistsAsLinkToFile', () { + DateTime before = downstairs(); + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + DateTime after = ceil(); + DateTime accessed = fs.file(ns('/bar')).lastAccessedSync(); + expect(accessed, isSameOrAfter(before)); + expect(accessed, isSameOrBefore(after)); + }); + }); + + group('setLastAccessed', () { + final DateTime time = DateTime(1999); + + test('throwsIfDoesntExist', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/foo')).setLastAccessedSync(time); + }); + }); + + test('throwsIfExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/foo')).setLastAccessedSync(time); + }); + }); + + test('succeedsIfExistsAsFile', () { + File f = fs.file(ns('/foo'))..createSync(); + f.setLastAccessedSync(time); + expect(fs.file(ns('/foo')).lastAccessedSync(), time); + }); + + test('succeedsIfExistsAsLinkToFile', () { + File f = fs.file(ns('/foo'))..createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + f.setLastAccessedSync(time); + expect(fs.file(ns('/bar')).lastAccessedSync(), time); + }); + }); + + group('lastModified', () { + test('isNowForNewlyCreatedFile', () { + DateTime before = downstairs(); + File f = fs.file(ns('/foo'))..createSync(); + DateTime after = ceil(); + DateTime modified = f.lastModifiedSync(); + expect(modified, isSameOrAfter(before)); + expect(modified, isSameOrBefore(after)); + }); + + test('throwsIfDoesntExist', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/foo')).lastModifiedSync(); + }); + }); + + test('throwsIfExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/foo')).lastModifiedSync(); + }); + }); + + test('succeedsIfExistsAsLinkToFile', () { + DateTime before = downstairs(); + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + DateTime after = ceil(); + DateTime modified = fs.file(ns('/bar')).lastModifiedSync(); + expect(modified, isSameOrAfter(before)); + expect(modified, isSameOrBefore(after)); + }); + }); + + group('setLastModified', () { + final DateTime time = DateTime(1999); + + test('throwsIfDoesntExist', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/foo')).setLastModifiedSync(time); + }); + }); + + test('throwsIfExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/foo')).setLastModifiedSync(time); + }); + }); + + test('succeedsIfExistsAsFile', () { + File f = fs.file(ns('/foo'))..createSync(); + f.setLastModifiedSync(time); + expect(fs.file(ns('/foo')).lastModifiedSync(), time); + }); + + test('succeedsIfExistsAsLinkToFile', () { + File f = fs.file(ns('/foo'))..createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + f.setLastModifiedSync(time); + expect(fs.file(ns('/bar')).lastModifiedSync(), time); + }); + }); + + group('open', () { + void testIfDoesntExistAtTail(FileMode mode) { + if (mode == FileMode.read) { + test('throwsIfDoesntExistAtTail', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/bar')).openSync(mode: mode); + }); + }); + } else { + test('createsFileIfDoesntExistAtTail', () { + RandomAccessFile raf = fs.file(ns('/bar')).openSync(mode: mode); + raf.closeSync(); + expect(fs.file(ns('/bar')), exists); + }); + } + } + + void testThrowsIfDoesntExistViaTraversal(FileMode mode) { + test('throwsIfDoesntExistViaTraversal', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/bar/baz')).openSync(mode: mode); + }); + }); + } + + void testRandomAccessFileOperations(FileMode mode) { + group('RandomAccessFile', () { + late File f; + late RandomAccessFile raf; + + setUp(() { + f = fs.file(ns('/foo'))..createSync(); + f.writeAsStringSync('pre-existing content\n', flush: true); + raf = f.openSync(mode: mode); + }); + + tearDown(() { + try { + raf.closeSync(); + } on FileSystemException { + // Ignore; a test may have already closed it. + } + }); + + test('succeedsIfClosedAfterClosed', () { + raf.closeSync(); + expectFileSystemException(null, () { + raf.closeSync(); + }); + }); + + test('throwsIfReadAfterClose', () { + raf.closeSync(); + expectFileSystemException(null, () { + raf.readByteSync(); + }); + }); + + test('throwsIfWriteAfterClose', () { + raf.closeSync(); + expectFileSystemException(null, () { + raf.writeByteSync(0xBAD); + }); + }); + + test('throwsIfTruncateAfterClose', () { + raf.closeSync(); + expectFileSystemException(null, () { + raf.truncateSync(0); + }); + }); + + if (mode == FileMode.write || mode == FileMode.writeOnly) { + test('lengthIsResetToZeroIfOpened', () { + expect(raf.lengthSync(), equals(0)); + }); + + test('throwsIfAsyncUnawaited', () async { + try { + final Future future = raf.flush(); + expectFileSystemException(null, () => raf.flush()); + expectFileSystemException(null, () => raf.flushSync()); + await expectLater(future, completes); + raf.flushSync(); + } finally { + raf.closeSync(); + } + }); + } else { + test('lengthIsNotModifiedIfOpened', () { + expect(raf.lengthSync(), isNot(equals(0))); + }); + } + + if (mode == FileMode.writeOnly || + mode == FileMode.writeOnlyAppend) { + test('throwsIfReadByte', () { + expectFileSystemException(ErrorCodes.EBADF, () { + raf.readByteSync(); + }); + }); + + test('throwsIfRead', () { + expectFileSystemException(ErrorCodes.EBADF, () { + raf.readSync(2); + }); + }); + + test('throwsIfReadInto', () { + expectFileSystemException(ErrorCodes.EBADF, () { + raf.readIntoSync(List.filled(5, 0)); + }); + }); + } else { + group('read', () { + setUp(() { + if (mode == FileMode.write) { + // Write data back that we truncated when opening the file. + raf.writeStringSync('pre-existing content\n'); + } + // Reset the position to zero so we can read the content. + raf.setPositionSync(0); + }); + + test('readByte', () { + expect(utf8.decode([raf.readByteSync()]), 'p'); + }); + + test('read', () { + List bytes = raf.readSync(1024); + expect(bytes.length, 21); + expect(utf8.decode(bytes), 'pre-existing content\n'); + }); + + test('readIntoWithBufferLargerThanContent', () { + List buffer = List.filled(1024, 0); + int numRead = raf.readIntoSync(buffer); + expect(numRead, 21); + expect(utf8.decode(buffer.sublist(0, 21)), + 'pre-existing content\n'); + }); + + test('readIntoWithBufferSmallerThanContent', () { + List buffer = List.filled(10, 0); + int numRead = raf.readIntoSync(buffer); + expect(numRead, 10); + expect(utf8.decode(buffer), 'pre-existi'); + }); + + test('readIntoWithStart', () { + List buffer = List.filled(10, 0); + int numRead = raf.readIntoSync(buffer, 2); + expect(numRead, 8); + expect(utf8.decode(buffer.sublist(2)), 'pre-exis'); + }); + + test('readIntoWithStartAndEnd', () { + List buffer = List.filled(10, 0); + int numRead = raf.readIntoSync(buffer, 2, 5); + expect(numRead, 3); + expect(utf8.decode(buffer.sublist(2, 5)), 'pre'); + }); + + test('openReadHandleDoesNotChange', () { + final String initial = utf8.decode(raf.readSync(4)); + expect(initial, 'pre-'); + final File newFile = f.renameSync(ns('/bar')); + String rest = utf8.decode(raf.readSync(1024)); + expect(rest, 'existing content\n'); + + assert(newFile.path != f.path); + expect(f, isNot(exists)); + expect(newFile, exists); + + // [RandomAccessFile.path] always returns the original path. + expect(raf.path, f.path); + }); + }); + } + + if (mode == FileMode.read) { + test('throwsIfWriteByte', () { + expectFileSystemException(ErrorCodes.EBADF, () { + raf.writeByteSync(0xBAD); + }); + }); + + test('throwsIfWriteFrom', () { + expectFileSystemException(ErrorCodes.EBADF, () { + raf.writeFromSync([1, 2, 3, 4]); + }); + }); + + test('throwsIfWriteString', () { + expectFileSystemException(ErrorCodes.EBADF, () { + raf.writeStringSync('This should throw.'); + }); + }); + } else { + test('lengthGrowsAsDataIsWritten', () { + int lengthBefore = f.lengthSync(); + raf.writeByteSync(0xFACE); + expect(raf.lengthSync(), lengthBefore + 1); + }); + + test('flush', () { + int lengthBefore = f.lengthSync(); + raf.writeByteSync(0xFACE); + raf.flushSync(); + expect(f.lengthSync(), lengthBefore + 1); + }); + + test('writeByte', () { + raf.writeByteSync(utf8.encode('A').first); + raf.flushSync(); + if (mode == FileMode.write || mode == FileMode.writeOnly) { + expect(f.readAsStringSync(), 'A'); + } else { + expect(f.readAsStringSync(), 'pre-existing content\nA'); + } + }); + + test('writeFrom', () { + raf.writeFromSync(utf8.encode('Hello world')); + raf.flushSync(); + if (mode == FileMode.write || mode == FileMode.writeOnly) { + expect(f.readAsStringSync(), 'Hello world'); + } else { + expect(f.readAsStringSync(), + 'pre-existing content\nHello world'); + } + }); + + test('writeFromWithStart', () { + raf.writeFromSync(utf8.encode('Hello world'), 2); + raf.flushSync(); + if (mode == FileMode.write || mode == FileMode.writeOnly) { + expect(f.readAsStringSync(), 'llo world'); + } else { + expect( + f.readAsStringSync(), 'pre-existing content\nllo world'); + } + }); + + test('writeFromWithStartAndEnd', () { + raf.writeFromSync(utf8.encode('Hello world'), 2, 5); + raf.flushSync(); + if (mode == FileMode.write || mode == FileMode.writeOnly) { + expect(f.readAsStringSync(), 'llo'); + } else { + expect(f.readAsStringSync(), 'pre-existing content\nllo'); + } + }); + + test('writeString', () { + raf.writeStringSync('Hello world'); + raf.flushSync(); + if (mode == FileMode.write || mode == FileMode.writeOnly) { + expect(f.readAsStringSync(), 'Hello world'); + } else { + expect(f.readAsStringSync(), + 'pre-existing content\nHello world'); + } + }); + + test('openWriteHandleDoesNotChange', () { + raf.writeStringSync('Hello '); + final File newFile = f.renameSync(ns('/bar')); + raf.writeStringSync('world'); + + final String contents = newFile.readAsStringSync(); + if (mode == FileMode.write || mode == FileMode.writeOnly) { + expect(contents, 'Hello world'); + } else { + expect(contents, 'pre-existing content\nHello world'); + } + + assert(newFile.path != f.path); + expect(f, isNot(exists)); + expect(newFile, exists); + + // [RandomAccessFile.path] always returns the original path. + expect(raf.path, f.path); + }); + } + + if (mode == FileMode.append || mode == FileMode.writeOnlyAppend) { + test('positionInitializedToEndOfFile', () { + expect(raf.positionSync(), 21); + }); + } else { + test('positionInitializedToZero', () { + expect(raf.positionSync(), 0); + }); + } + + group('position', () { + setUp(() { + if (mode == FileMode.write || mode == FileMode.writeOnly) { + // Write data back that we truncated when opening the file. + raf.writeStringSync('pre-existing content\n'); + } + }); + + if (mode != FileMode.writeOnly && + mode != FileMode.writeOnlyAppend) { + test('growsAfterRead', () { + raf.setPositionSync(0); + raf.readSync(10); + expect(raf.positionSync(), 10); + }); + + test('affectsRead', () { + raf.setPositionSync(5); + expect(utf8.decode(raf.readSync(5)), 'xisti'); + }); + } + + if (mode == FileMode.read) { + test('succeedsIfSetPastEndOfFile', () { + raf.setPositionSync(32); + expect(raf.positionSync(), 32); + }); + } else { + test('growsAfterWrite', () { + int positionBefore = raf.positionSync(); + raf.writeStringSync('Hello world'); + expect(raf.positionSync(), positionBefore + 11); + }); + + test('affectsWrite', () { + raf.setPositionSync(5); + raf.writeStringSync('-yo-'); + raf.flushSync(); + expect(f.readAsStringSync(), 'pre-e-yo-ing content\n'); + }); + + test('succeedsIfSetAndWrittenPastEndOfFile', () { + raf.setPositionSync(32); + expect(raf.positionSync(), 32); + raf.writeStringSync('here'); + raf.flushSync(); + List bytes = f.readAsBytesSync(); + expect(bytes.length, 36); + expect(utf8.decode(bytes.sublist(0, 21)), + 'pre-existing content\n'); + expect(utf8.decode(bytes.sublist(32, 36)), 'here'); + expect(bytes.sublist(21, 32), everyElement(0)); + }); + } + + test('throwsIfSetToNegativeNumber', () { + expectFileSystemException(ErrorCodes.EINVAL, () { + raf.setPositionSync(-12); + }); + }); + }); + + if (mode == FileMode.read) { + test('throwsIfTruncate', () { + expectFileSystemException(ErrorCodes.EINVAL, () { + raf.truncateSync(5); + }); + }); + } else { + group('truncate', () { + setUp(() { + if (mode == FileMode.write || mode == FileMode.writeOnly) { + // Write data back that we truncated when opening the file. + raf.writeStringSync('pre-existing content\n'); + } + }); + + test('succeedsIfSetWithinRangeOfContent', () { + raf.truncateSync(5); + raf.flushSync(); + expect(f.lengthSync(), 5); + expect(f.readAsStringSync(), 'pre-e'); + }); + + test('succeedsIfSetToZero', () { + raf.truncateSync(0); + raf.flushSync(); + expect(f.lengthSync(), 0); + expect(f.readAsStringSync(), isEmpty); + }); + + test('throwsIfSetToNegativeNumber', () { + expectFileSystemException(ErrorCodes.EINVAL, () { + raf.truncateSync(-2); + }); + }); + + test('extendsFileIfSetPastEndOfFile', () { + raf.truncateSync(32); + raf.flushSync(); + List bytes = f.readAsBytesSync(); + expect(bytes.length, 32); + expect(utf8.decode(bytes.sublist(0, 21)), + 'pre-existing content\n'); + expect(bytes.sublist(21, 32), everyElement(0)); + }); + }); + } + }); + } + + void testOpenWithMode(FileMode mode) { + testIfDoesntExistAtTail(mode); + testThrowsIfDoesntExistViaTraversal(mode); + testRandomAccessFileOperations(mode); + } + + group('READ', () => testOpenWithMode(FileMode.read)); + group('WRITE', () => testOpenWithMode(FileMode.write)); + group('APPEND', () => testOpenWithMode(FileMode.append)); + group('WRITE_ONLY', () => testOpenWithMode(FileMode.writeOnly)); + group('WRITE_ONLY_APPEND', + () => testOpenWithMode(FileMode.writeOnlyAppend)); + }); + + group('openRead', () { + test('throwsIfDoesntExist', () { + Stream> stream = fs.file(ns('/foo')).openRead(); + expect(stream.drain(), + throwsFileSystemException(ErrorCodes.ENOENT)); + }); + + test('succeedsIfExistsAsFile', () async { + File f = fs.file(ns('/foo'))..createSync(); + f.writeAsStringSync('Hello world', flush: true); + Stream> stream = f.openRead(); + List> data = await stream.toList(); + expect(data, hasLength(1)); + expect(utf8.decode(data[0]), 'Hello world'); + }); + + test('throwsIfExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + Stream> stream = fs.file(ns('/foo')).openRead(); + expect(stream.drain(), + throwsFileSystemException(ErrorCodes.EISDIR)); + }); + + test('succeedsIfExistsAsLinkToFile', () async { + File f = fs.file(ns('/foo'))..createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + f.writeAsStringSync('Hello world', flush: true); + Stream> stream = fs.file(ns('/bar')).openRead(); + List> data = await stream.toList(); + expect(data, hasLength(1)); + expect(utf8.decode(data[0]), 'Hello world'); + }); + + test('respectsStartAndEndParameters', () async { + File f = fs.file(ns('/foo'))..createSync(); + f.writeAsStringSync('Hello world', flush: true); + Stream> stream = f.openRead(2); + List> data = await stream.toList(); + expect(data, hasLength(1)); + expect(utf8.decode(data[0]), 'llo world'); + stream = f.openRead(2, 5); + data = await stream.toList(); + expect(data, hasLength(1)); + expect(utf8.decode(data[0]), 'llo'); + }); + + test('throwsIfStartParameterIsNegative', () async { + File f = fs.file(ns('/foo'))..createSync(); + Stream> stream = f.openRead(-2); + expect(stream.drain(), throwsRangeError); + }); + + test('stopsAtEndOfFileIfEndParameterIsPastEndOfFile', () async { + File f = fs.file(ns('/foo'))..createSync(); + f.writeAsStringSync('Hello world', flush: true); + Stream> stream = f.openRead(2, 1024); + List> data = await stream.toList(); + expect(data, hasLength(1)); + expect(utf8.decode(data[0]), 'llo world'); + }); + + test('providesSingleSubscriptionStream', () async { + File f = fs.file(ns('/foo'))..createSync(); + f.writeAsStringSync('Hello world', flush: true); + Stream> stream = f.openRead(); + expect(stream.isBroadcast, isFalse); + await stream.drain(); + }); + + test('openReadHandleDoesNotChange', () async { + // Ideally, `data` should be large enough so that its contents are + // split across multiple chunks in the [Stream]. However, there + // doesn't seem to be a good way to determine the chunk size used by + // [io.File]. + final List data = List.generate( + 1024 * 256, + (int index) => index & 0xFF, + growable: false, + ); + + final File f = fs.file(ns('/foo'))..createSync(); + + f.writeAsBytesSync(data, flush: true); + final Stream> stream = f.openRead(); + + File? newFile; + List? initialChunk; + final List remainingChunks = []; + + await for (List chunk in stream) { + if (initialChunk == null) { + initialChunk = chunk; + assert(initialChunk.isNotEmpty); + expect(initialChunk, data.getRange(0, initialChunk.length)); + + newFile = f.renameSync(ns('/bar')); + } else { + remainingChunks.addAll(chunk); + } + } + + expect( + remainingChunks, + data.getRange(initialChunk!.length, data.length), + ); + + assert(newFile?.path != f.path); + expect(f, isNot(exists)); + expect(newFile, exists); + }); + + test('openReadCompatibleWithUtf8Decoder', () async { + const content = 'Hello world!'; + File file = fs.file(ns('/foo')) + ..createSync() + ..writeAsStringSync(content); + expect( + await file + .openRead() + .transform(utf8.decoder) + .transform(const LineSplitter()) + .first, + content, + ); + }); + }); + + group('openWrite', () { + test('createsFileIfDoesntExist', () async { + await fs.file(ns('/foo')).openWrite().close(); + expect(fs.file(ns('/foo')), exists); + }); + + test('throwsIfExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + expect(fs.file(ns('/foo')).openWrite().close(), + throwsFileSystemException(ErrorCodes.EISDIR)); + }); + + test('throwsIfExistsAsLinkToDirectory', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expect(fs.file(ns('/bar')).openWrite().close(), + throwsFileSystemException(ErrorCodes.EISDIR)); + }); + + test('throwsIfModeIsRead', () { + expect(() => fs.file(ns('/foo')).openWrite(mode: FileMode.read), + throwsArgumentError); + }); + + test('succeedsIfExistsAsEmptyFile', () async { + File f = fs.file(ns('/foo'))..createSync(); + IOSink sink = f.openWrite(); + sink.write('Hello world'); + await sink.flush(); + await sink.close(); + expect(f.readAsStringSync(), 'Hello world'); + }); + + test('succeedsIfExistsAsLinkToFile', () async { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + IOSink sink = fs.file(ns('/bar')).openWrite(); + sink.write('Hello world'); + await sink.flush(); + await sink.close(); + expect(fs.file(ns('/foo')).readAsStringSync(), 'Hello world'); + }); + + test('overwritesContentInWriteMode', () async { + File f = fs.file(ns('/foo'))..createSync(); + f.writeAsStringSync('Hello'); + IOSink sink = f.openWrite(); + sink.write('Goodbye'); + await sink.flush(); + await sink.close(); + expect(fs.file(ns('/foo')).readAsStringSync(), 'Goodbye'); + }); + + test('appendsContentInAppendMode', () async { + File f = fs.file(ns('/foo'))..createSync(); + f.writeAsStringSync('Hello'); + IOSink sink = f.openWrite(mode: FileMode.append); + sink.write('Goodbye'); + await sink.flush(); + await sink.close(); + expect(fs.file(ns('/foo')).readAsStringSync(), 'HelloGoodbye'); + }); + + test('openWriteHandleDoesNotChange', () async { + File f = fs.file(ns('/foo'))..createSync(); + IOSink sink = f.openWrite(); + sink.write('Hello'); + await sink.flush(); + + final File newFile = f.renameSync(ns('/bar')); + sink.write('Goodbye'); + await sink.flush(); + await sink.close(); + + expect(newFile.readAsStringSync(), 'HelloGoodbye'); + + assert(newFile.path != f.path); + expect(f, isNot(exists)); + expect(newFile, exists); + }); + + group('ioSink', () { + late File f; + late IOSink sink; + late bool isSinkClosed; + + Future closeSink() { + Future future = sink.close(); + isSinkClosed = true; + return future; + } + + setUp(() { + f = fs.file(ns('/foo')); + sink = f.openWrite(); + isSinkClosed = false; + }); + + tearDown(() async { + if (!isSinkClosed) { + await closeSink(); + } + }); + + test('throwsIfAddError', () async { + sink.addError(ArgumentError()); + expect(sink.done, throwsArgumentError); + isSinkClosed = true; + }); + + test('allowsChangingEncoding', () async { + sink.encoding = latin1; + sink.write('ÿ'); + sink.encoding = utf8; + sink.write('ÿ'); + await sink.flush(); + expect(await f.readAsBytes(), [255, 195, 191]); + }); + + test('succeedsIfAddRawData', () async { + sink.add([1, 2, 3, 4]); + await sink.flush(); + expect(await f.readAsBytes(), [1, 2, 3, 4]); + }); + + test('succeedsIfWrite', () async { + sink.write('Hello world'); + await sink.flush(); + expect(await f.readAsString(), 'Hello world'); + }); + + test('succeedsIfWriteAll', () async { + sink.writeAll(['foo', 'bar', 'baz'], ' '); + await sink.flush(); + expect(await f.readAsString(), 'foo bar baz'); + }); + + test('succeedsIfWriteCharCode', () async { + sink.writeCharCode(35); + await sink.flush(); + expect(await f.readAsString(), '#'); + }); + + test('succeedsIfWriteln', () async { + sink.writeln('Hello world'); + await sink.flush(); + expect(await f.readAsString(), 'Hello world\n'); + }); + + test('ignoresDataWrittenAfterClose', () async { + sink.write('Before close'); + await closeSink(); + expect(() => sink.write('After close'), throwsStateError); + expect(await f.readAsString(), 'Before close'); + }); + + test('ignoresCloseAfterAlreadyClosed', () async { + sink.write('Hello world'); + Future f1 = closeSink(); + Future f2 = closeSink(); + await Future.wait(>[f1, f2]); + }); + + test('returnsAccurateDoneFuture', () async { + bool done = false; + // ignore: unawaited_futures + sink.done.then((dynamic _) => done = true); + expect(done, isFalse); + sink.write('foo'); + expect(done, isFalse); + await sink.close(); + expect(done, isTrue); + }); + + group('addStream', () { + late StreamController> controller; + late bool isControllerClosed; + + Future closeController() { + Future future = controller.close(); + isControllerClosed = true; + return future; + } + + setUp(() { + controller = StreamController>(); + isControllerClosed = false; + sink.addStream(controller.stream); + }); + + tearDown(() async { + if (!isControllerClosed) { + await closeController(); + } + }); + + test('succeedsIfStreamProducesData', () async { + controller.add([1, 2, 3, 4, 5]); + await closeController(); + await sink.flush(); + expect(await f.readAsBytes(), [1, 2, 3, 4, 5]); + }); + + test('blocksCallToAddWhileStreamIsActive', () { + expect(() => sink.add([1, 2, 3]), throwsStateError); + }); + + test('blocksCallToWriteWhileStreamIsActive', () { + expect(() => sink.write('foo'), throwsStateError); + }); + + test('blocksCallToWriteAllWhileStreamIsActive', () { + expect(() => sink.writeAll(['a', 'b']), throwsStateError); + }); + + test('blocksCallToWriteCharCodeWhileStreamIsActive', () { + expect(() => sink.writeCharCode(35), throwsStateError); + }); + + test('blocksCallToWritelnWhileStreamIsActive', () { + expect(() => sink.writeln('foo'), throwsStateError); + }); + + test('blocksCallToFlushWhileStreamIsActive', () { + expect(() => sink.flush(), throwsStateError); + }); + }); + }); + }); + + group('readAsBytes', () { + test('throwsIfDoesntExist', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/foo')).readAsBytesSync(); + }); + }); + + test('throwsIfExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/foo')).readAsBytesSync(); + }); + }); + + test('throwsIfExistsAsLinkToDirectory', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/bar')).readAsBytesSync(); + }); + }); + + test('succeedsIfExistsAsFile', () { + File f = fs.file(ns('/foo'))..createSync(); + f.writeAsBytesSync([1, 2, 3, 4]); + expect(f.readAsBytesSync(), [1, 2, 3, 4]); + }); + + test('succeedsIfExistsAsLinkToFile', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + fs.file(ns('/foo')).writeAsBytesSync([1, 2, 3, 4]); + expect(fs.file(ns('/bar')).readAsBytesSync(), [1, 2, 3, 4]); + }); + + test('returnsEmptyListForZeroByteFile', () { + File f = fs.file(ns('/foo'))..createSync(); + expect(f.readAsBytesSync(), isEmpty); + }); + + test('returns a copy, not a view, of the file content', () { + File f = fs.file(ns('/foo'))..createSync(); + f.writeAsBytesSync([1, 2, 3, 4]); + List result = f.readAsBytesSync(); + expect(result, [1, 2, 3, 4]); + result[0] = 10; + expect(f.readAsBytesSync(), [1, 2, 3, 4]); + }); + }); + + group('readAsString', () { + test('throwsIfDoesntExist', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/foo')).readAsStringSync(); + }); + }); + + test('throwsIfExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/foo')).readAsStringSync(); + }); + }); + + test('throwsIfExistsAsLinkToDirectory', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/bar')).readAsStringSync(); + }); + }); + + test('succeedsIfExistsAsFile', () { + File f = fs.file(ns('/foo'))..createSync(); + f.writeAsStringSync('Hello world'); + expect(f.readAsStringSync(), 'Hello world'); + }); + + test('succeedsIfExistsAsLinkToFile', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + fs.file(ns('/foo')).writeAsStringSync('Hello world'); + expect(fs.file(ns('/bar')).readAsStringSync(), 'Hello world'); + }); + + test('returnsEmptyStringForZeroByteFile', () { + File f = fs.file(ns('/foo'))..createSync(); + expect(f.readAsStringSync(), isEmpty); + }); + }); + + group('readAsLines', () { + const String testString = 'Hello world\nHow are you?\nI am fine'; + final List expectedLines = [ + 'Hello world', + 'How are you?', + 'I am fine', + ]; + + test('throwsIfDoesntExist', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/foo')).readAsLinesSync(); + }); + }); + + test('throwsIfExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/foo')).readAsLinesSync(); + }); + }); + + test('throwsIfExistsAsLinkToDirectory', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/bar')).readAsLinesSync(); + }); + }); + + test('succeedsIfExistsAsFile', () { + File f = fs.file(ns('/foo'))..createSync(); + f.writeAsStringSync(testString); + expect(f.readAsLinesSync(), expectedLines); + }); + + test('succeedsIfExistsAsLinkToFile', () { + File f = fs.file(ns('/foo'))..createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + f.writeAsStringSync(testString); + expect(f.readAsLinesSync(), expectedLines); + }); + + test('returnsEmptyListForZeroByteFile', () { + File f = fs.file(ns('/foo'))..createSync(); + expect(f.readAsLinesSync(), isEmpty); + }); + + test('isTrailingNewlineAgnostic', () { + File f = fs.file(ns('/foo'))..createSync(); + f.writeAsStringSync('$testString\n'); + expect(f.readAsLinesSync(), expectedLines); + + f.writeAsStringSync('\n'); + expect(f.readAsLinesSync(), ['']); + + f.writeAsStringSync('\n\n'); + expect(f.readAsLinesSync(), ['', '']); + }); + }); + + group('writeAsBytes', () { + test('returnsCovariantType', () async { + expect(await fs.file(ns('/foo')).writeAsBytes([]), isFile); + }); + + test('createsFileIfDoesntExist', () { + File f = fs.file(ns('/foo')); + expect(f, isNot(exists)); + f.writeAsBytesSync([1, 2, 3, 4]); + expect(f, exists); + }); + + test('throwsIfExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/foo')).writeAsBytesSync([1, 2, 3, 4]); + }); + }); + + test('throwsIfExistsAsLinkToDirectory', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/foo')).writeAsBytesSync([1, 2, 3, 4]); + }); + }); + + test('succeedsIfExistsAsLinkToFile', () { + File f = fs.file(ns('/foo'))..createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + fs.file(ns('/bar')).writeAsBytesSync([1, 2, 3, 4]); + expect(f.readAsBytesSync(), [1, 2, 3, 4]); + }); + + test('throwsIfFileModeRead', () { + File f = fs.file(ns('/foo'))..createSync(); + expectFileSystemException(ErrorCodes.EBADF, () { + f.writeAsBytesSync([1], mode: FileMode.read); + }); + }); + + test('overwritesContentIfFileModeWrite', () { + File f = fs.file(ns('/foo'))..createSync(); + f.writeAsBytesSync([1, 2]); + expect(f.readAsBytesSync(), [1, 2]); + f.writeAsBytesSync([3, 4]); + expect(f.readAsBytesSync(), [3, 4]); + }); + + test('appendsContentIfFileModeAppend', () { + File f = fs.file(ns('/foo'))..createSync(); + f.writeAsBytesSync([1, 2], mode: FileMode.append); + expect(f.readAsBytesSync(), [1, 2]); + f.writeAsBytesSync([3, 4], mode: FileMode.append); + expect(f.readAsBytesSync(), [1, 2, 3, 4]); + }); + + test('acceptsEmptyBytesList', () { + File f = fs.file(ns('/foo'))..createSync(); + f.writeAsBytesSync([]); + expect(f.readAsBytesSync(), []); + }); + + test('updatesLastModifiedTime', () async { + File f = fs.file(ns('/foo'))..createSync(); + DateTime before = f.statSync().modified; + await Future.delayed(const Duration(seconds: 2)); + f.writeAsBytesSync([1, 2, 3]); + DateTime after = f.statSync().modified; + expect(after, isAfter(before)); + }); + }); + + group('writeAsString', () { + test('returnsCovariantType', () async { + expect(await fs.file(ns('/foo')).writeAsString('foo'), isFile); + }); + + test('createsFileIfDoesntExist', () { + File f = fs.file(ns('/foo')); + expect(f, isNot(exists)); + f.writeAsStringSync('Hello world'); + expect(f, exists); + }); + + test('throwsIfExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/foo')).writeAsStringSync('Hello world'); + }); + }); + + test('throwsIfExistsAsLinkToDirectory', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/foo')).writeAsStringSync('Hello world'); + }); + }); + + test('succeedsIfExistsAsLinkToFile', () { + File f = fs.file(ns('/foo'))..createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + fs.file(ns('/bar')).writeAsStringSync('Hello world'); + expect(f.readAsStringSync(), 'Hello world'); + }); + + test('throwsIfFileModeRead', () { + File f = fs.file(ns('/foo'))..createSync(); + expectFileSystemException(ErrorCodes.EBADF, () { + f.writeAsStringSync('Hello world', mode: FileMode.read); + }); + }); + + test('overwritesContentIfFileModeWrite', () { + File f = fs.file(ns('/foo'))..createSync(); + f.writeAsStringSync('Hello world'); + expect(f.readAsStringSync(), 'Hello world'); + f.writeAsStringSync('Goodbye cruel world'); + expect(f.readAsStringSync(), 'Goodbye cruel world'); + }); + + test('appendsContentIfFileModeAppend', () { + File f = fs.file(ns('/foo'))..createSync(); + f.writeAsStringSync('Hello', mode: FileMode.append); + expect(f.readAsStringSync(), 'Hello'); + f.writeAsStringSync('Goodbye', mode: FileMode.append); + expect(f.readAsStringSync(), 'HelloGoodbye'); + }); + + test('acceptsEmptyString', () { + File f = fs.file(ns('/foo'))..createSync(); + f.writeAsStringSync(''); + expect(f.readAsStringSync(), isEmpty); + }); + }); + + group('exists', () { + test('trueIfExists', () { + fs.file(ns('/foo')).createSync(); + expect(fs.file(ns('/foo')), exists); + }); + + test('falseIfDoesntExistAtTail', () { + expect(fs.file(ns('/foo')), isNot(exists)); + }); + + test('falseIfDoesntExistViaTraversal', () { + expect(fs.file(ns('/foo/bar')), isNot(exists)); + }); + + test('falseIfExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + expect(fs.file(ns('/foo')), isNot(exists)); + }); + + test('falseIfExistsAsLinkToDirectory', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expect(fs.file(ns('/bar')), isNot(exists)); + }); + + test('trueIfExistsAsLinkToFile', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expect(fs.file(ns('/bar')), exists); + }); + + test('falseIfNotFoundSegmentExistsThenIsBackedOut', () { + fs.file(ns('/foo/bar')).createSync(recursive: true); + expect(fs.directory(ns('/baz/../foo/bar')), isNot(exists)); + }); + }); + + group('stat', () { + test('isNotFoundIfDoesntExistAtTail', () { + FileStat stat = fs.file(ns('/foo')).statSync(); + expect(stat.type, FileSystemEntityType.notFound); + }); + + test('isNotFoundIfDoesntExistViaTraversal', () { + FileStat stat = fs.file(ns('/foo/bar')).statSync(); + expect(stat.type, FileSystemEntityType.notFound); + }); + + test('isDirectoryIfExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + FileStat stat = fs.file(ns('/foo')).statSync(); + expect(stat.type, FileSystemEntityType.directory); + }); + + test('isFileIfExistsAsFile', () { + fs.file(ns('/foo')).createSync(); + FileStat stat = fs.file(ns('/foo')).statSync(); + expect(stat.type, FileSystemEntityType.file); + }); + + test('isFileIfExistsAsLinkToFile', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + FileStat stat = fs.file(ns('/bar')).statSync(); + expect(stat.type, FileSystemEntityType.file); + }); + }); + + group('delete', () { + test('returnsCovariantType', () async { + File f = fs.file(ns('/foo'))..createSync(); + expect(await f.delete(), isFile); + }); + + test('succeedsIfExistsAsFile', () { + fs.file(ns('/foo')).createSync(); + expect(fs.file(ns('/foo')), exists); + fs.file(ns('/foo')).deleteSync(); + expect(fs.file(ns('/foo')), isNot(exists)); + }); + + test('throwsIfDoesntExistAndRecursiveFalse', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/foo')).deleteSync(); + }); + }); + + test('throwsIfDoesntExistAndRecursiveTrue', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.file(ns('/foo')).deleteSync(recursive: true); + }); + }); + + test('succeedsIfExistsAsDirectoryAndRecursiveTrue', () { + fs.directory(ns('/foo')).createSync(); + fs.file(ns('/foo')).deleteSync(recursive: true); + expect(fs.typeSync(ns('/foo')), FileSystemEntityType.notFound); + }); + + test('throwsIfExistsAsDirectoryAndRecursiveFalse', () { + fs.directory(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/foo')).deleteSync(); + }); + }); + + test('succeedsIfExistsAsLinkToFileAndRecursiveTrue', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expect(fs.file(ns('/bar')), exists); + fs.file(ns('/bar')).deleteSync(recursive: true); + expect(fs.file(ns('/bar')), isNot(exists)); + }); + + test('succeedsIfExistsAsLinkToFileAndRecursiveFalse', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expect(fs.file(ns('/bar')), exists); + fs.file(ns('/bar')).deleteSync(); + expect(fs.file(ns('/bar')), isNot(exists)); + }); + + test('succeedsIfExistsAsLinkToDirectoryAndRecursiveTrue', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expect(fs.typeSync(ns('/bar')), FileSystemEntityType.directory); + fs.file(ns('/bar')).deleteSync(recursive: true); + expect(fs.typeSync(ns('/foo')), FileSystemEntityType.directory); + expect(fs.typeSync(ns('/bar')), FileSystemEntityType.notFound); + }); + + test('throwsIfExistsAsLinkToDirectoryAndRecursiveFalse', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/bar')).createSync(ns('/foo')); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.file(ns('/bar')).deleteSync(); + }); + }); + }); + }); + + group('Link', () { + group('uri', () { + test('whenTargetIsDirectory', () { + fs.directory(ns('/foo')).createSync(); + Link l = fs.link(ns('/bar'))..createSync(ns('/foo')); + expect(l.uri, fs.path.toUri(ns('/bar'))); + expect(fs.link('bar').uri.toString(), 'bar'); + }); + + test('whenTargetIsFile', () { + fs.file(ns('/foo')).createSync(); + Link l = fs.link(ns('/bar'))..createSync(ns('/foo')); + expect(l.uri, fs.path.toUri(ns('/bar'))); + expect(fs.link('bar').uri.toString(), 'bar'); + }); + + test('whenLinkDoesntExist', () { + expect(fs.link(ns('/foo')).uri, fs.path.toUri(ns('/foo'))); + expect(fs.link('foo').uri.toString(), 'foo'); + }); + }); + + group('exists', () { + test('isFalseIfLinkDoesntExistAtTail', () { + expect(fs.link(ns('/foo')), isNot(exists)); + }); + + test('isFalseIfLinkDoesntExistViaTraversal', () { + expect(fs.link(ns('/foo/bar')), isNot(exists)); + }); + + test('isFalseIfPathReferencesFile', () { + fs.file(ns('/foo')).createSync(); + expect(fs.link(ns('/foo')), isNot(exists)); + }); + + test('isFalseIfPathReferencesDirectory', () { + fs.directory(ns('/foo')).createSync(); + expect(fs.link(ns('/foo')), isNot(exists)); + }); + + test('isTrueIfTargetIsNotFound', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + expect(l, exists); + }); + + test('isTrueIfTargetIsFile', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.file(ns('/bar')).createSync(); + expect(l, exists); + }); + + test('isTrueIfTargetIsDirectory', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.directory(ns('/bar')).createSync(); + expect(l, exists); + }); + + test('isTrueIfTargetIsLinkLoop', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.link(ns('/bar')).createSync(ns('/foo')); + expect(l, exists); + }); + }); + + group('stat', () { + test('isNotFoundIfLinkDoesntExistAtTail', () { + expect(fs.link(ns('/foo')).statSync().type, + FileSystemEntityType.notFound); + }); + + test('isNotFoundIfLinkDoesntExistViaTraversal', () { + expect(fs.link(ns('/foo/bar')).statSync().type, + FileSystemEntityType.notFound); + }); + + test('isFileIfPathReferencesFile', () { + fs.file(ns('/foo')).createSync(); + expect( + fs.link(ns('/foo')).statSync().type, FileSystemEntityType.file); + }); + + test('isDirectoryIfPathReferencesDirectory', () { + fs.directory(ns('/foo')).createSync(); + expect(fs.link(ns('/foo')).statSync().type, + FileSystemEntityType.directory); + }); + + test('isNotFoundIfTargetNotFoundAtTail', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + expect(l.statSync().type, FileSystemEntityType.notFound); + }); + + test('isNotFoundIfTargetNotFoundViaTraversal', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar/baz')); + expect(l.statSync().type, FileSystemEntityType.notFound); + }); + + test('isNotFoundIfTargetIsLinkLoop', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.link(ns('/bar')).createSync(ns('/foo')); + expect(l.statSync().type, FileSystemEntityType.notFound); + }); + + test('isFileIfTargetIsFile', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.file(ns('/bar')).createSync(); + expect(l.statSync().type, FileSystemEntityType.file); + }); + + test('isDirectoryIfTargetIsDirectory', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.directory(ns('/bar')).createSync(); + expect(l.statSync().type, FileSystemEntityType.directory); + }); + }); + + group('delete', () { + test('returnsCovariantType', () async { + Link link = fs.link(ns('/foo'))..createSync(ns('/bar')); + expect(await link.delete(), isLink); + }); + + test('throwsIfLinkDoesntExistAtTail', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.link(ns('/foo')).deleteSync(); + }); + }); + + test('throwsIfLinkDoesntExistViaTraversal', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.link(ns('/foo/bar')).deleteSync(); + }); + }); + + test('throwsIfPathReferencesFileAndRecursiveFalse', () { + fs.file(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EINVAL, () { + fs.link(ns('/foo')).deleteSync(); + }); + }); + + test('succeedsIfPathReferencesFileAndRecursiveTrue', () { + fs.file(ns('/foo')).createSync(); + fs.link(ns('/foo')).deleteSync(recursive: true); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.notFound); + }); + + test('throwsIfPathReferencesDirectoryAndRecursiveFalse', () { + fs.directory(ns('/foo')).createSync(); + // TODO(tvolkert): Change this to just be 'Is a directory' + // once Dart 1.22 is stable. + expectFileSystemException( + anyOf(ErrorCodes.EINVAL, ErrorCodes.EISDIR), + () { + fs.link(ns('/foo')).deleteSync(); + }, + ); + }); + + test('succeedsIfPathReferencesDirectoryAndRecursiveTrue', () { + fs.directory(ns('/foo')).createSync(); + fs.link(ns('/foo')).deleteSync(recursive: true); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.notFound); + }); + + test('unlinksIfTargetIsFileAndRecursiveFalse', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.file(ns('/bar')).createSync(); + l.deleteSync(); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.notFound); + expect(fs.typeSync(ns('/bar'), followLinks: false), + FileSystemEntityType.file); + }); + + test('unlinksIfTargetIsFileAndRecursiveTrue', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.file(ns('/bar')).createSync(); + l.deleteSync(recursive: true); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.notFound); + expect(fs.typeSync(ns('/bar'), followLinks: false), + FileSystemEntityType.file); + }); + + test('unlinksIfTargetIsDirectoryAndRecursiveFalse', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.directory(ns('/bar')).createSync(); + l.deleteSync(); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.notFound); + expect(fs.typeSync(ns('/bar'), followLinks: false), + FileSystemEntityType.directory); + }); + + test('unlinksIfTargetIsDirectoryAndRecursiveTrue', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.directory(ns('/bar')).createSync(); + l.deleteSync(recursive: true); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.notFound); + expect(fs.typeSync(ns('/bar'), followLinks: false), + FileSystemEntityType.directory); + }); + + test('unlinksIfTargetIsLinkLoop', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.link(ns('/bar')).createSync(ns('/foo')); + l.deleteSync(); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.notFound); + expect(fs.typeSync(ns('/bar'), followLinks: false), + FileSystemEntityType.link); + }); + }); + + group('parent', () { + test('returnsCovariantType', () { + expect(fs.link(ns('/foo')).parent, isDirectory); + }); + + test('succeedsIfLinkDoesntExist', () { + expect(fs.link(ns('/foo')).parent.path, ns('/')); + }); + + test('ignoresLinkTarget', () { + Link l = fs.link(ns('/foo/bar')) + ..createSync(ns('/baz/qux'), recursive: true); + expect(l.parent.path, ns('/foo')); + }); + }); + + group('create', () { + test('returnsCovariantType', () async { + expect(await fs.link(ns('/foo')).create(ns('/bar')), isLink); + }); + + test('succeedsIfLinkDoesntExistAtTail', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.link); + expect(l.targetSync(), ns('/bar')); + }); + + test('throwsIfLinkDoesntExistViaTraversalAndRecursiveFalse', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.link(ns('/foo/bar')).createSync('baz'); + }); + }); + + test('succeedsIfLinkDoesntExistViaTraversalAndRecursiveTrue', () { + Link l = fs.link(ns('/foo/bar'))..createSync('baz', recursive: true); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.directory); + expect(fs.typeSync(ns('/foo/bar'), followLinks: false), + FileSystemEntityType.link); + expect(l.targetSync(), 'baz'); + }); + + test('throwsIfAlreadyExistsAsFile', () { + fs.file(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EEXIST, () { + fs.link(ns('/foo')).createSync(ns('/bar')); + }); + }); + + test('throwsIfAlreadyExistsAsDirectory', () { + fs.directory(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EEXIST, () { + fs.link(ns('/foo')).createSync(ns('/bar')); + }); + }); + + test('throwsIfAlreadyExistsWithSameTarget', () { + fs.link(ns('/foo')).createSync(ns('/bar')); + expectFileSystemException(ErrorCodes.EEXIST, () { + fs.link(ns('/foo')).createSync(ns('/bar')); + }); + }); + + test('throwsIfAlreadyExistsWithDifferentTarget', () { + fs.link(ns('/foo')).createSync(ns('/bar')); + expectFileSystemException(ErrorCodes.EEXIST, () { + fs.link(ns('/foo')).createSync(ns('/baz')); + }); + }); + }); + + group('update', () { + test('returnsCovariantType', () async { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + expect(await l.update(ns('/baz')), isLink); + }); + + test('throwsIfLinkDoesntExistAtTail', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.link(ns('/foo')).updateSync(ns('/bar')); + }); + }); + + test('throwsIfLinkDoesntExistViaTraversal', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.link(ns('/foo/bar')).updateSync(ns('/baz')); + }); + }); + + test('throwsIfPathReferencesFile', () { + fs.file(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EINVAL, () { + fs.link(ns('/foo')).updateSync(ns('/bar')); + }); + }); + + test('throwsIfPathReferencesDirectory', () { + fs.directory(ns('/foo')).createSync(); + // TODO(tvolkert): Change this to just be 'Is a directory' + // once Dart 1.22 is stable. + expectFileSystemException( + anyOf(ErrorCodes.EINVAL, ErrorCodes.EISDIR), + () { + fs.link(ns('/foo')).updateSync(ns('/bar')); + }, + ); + }); + + test('succeedsIfNewTargetSameAsOldTarget', () { + fs.link(ns('/foo')).createSync(ns('/bar')); + fs.link(ns('/foo')).updateSync(ns('/bar')); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.link); + expect(fs.link(ns('/foo')).targetSync(), ns('/bar')); + }); + + test('succeedsIfNewTargetDifferentFromOldTarget', () { + fs.link(ns('/foo')).createSync(ns('/bar')); + fs.link(ns('/foo')).updateSync(ns('/baz')); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.link); + expect(fs.link(ns('/foo')).targetSync(), ns('/baz')); + }); + }); + + group('absolute', () { + test('returnsCovariantType', () { + expect(fs.link('foo').absolute, isLink); + }); + + test('returnsSamePathIfAlreadyAbsolute', () { + expect(fs.link(ns('/foo')).absolute.path, ns('/foo')); + }); + + test('succeedsForRelativePaths', () { + expect(fs.link('foo').absolute.path, ns('/foo')); + }); + }); + + group('target', () { + test('throwsIfLinkDoesntExistAtTail', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.link(ns('/foo')).targetSync(); + }); + }); + + test('throwsIfLinkDoesntExistViaTraversal', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.link(ns('/foo/bar')).targetSync(); + }); + }); + + test('throwsIfPathReferencesFile', () { + fs.file(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.link(ns('/foo')).targetSync(); + }); + }); + + test('throwsIfPathReferencesDirectory', () { + fs.directory(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.link(ns('/foo')).targetSync(); + }); + }); + + test('succeedsIfTargetIsNotFound', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + expect(l.targetSync(), ns('/bar')); + }); + + test('succeedsIfTargetIsFile', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.file(ns('/bar')).createSync(); + expect(l.targetSync(), ns('/bar')); + }); + + test('succeedsIfTargetIsDirectory', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.directory(ns('/bar')).createSync(); + expect(l.targetSync(), ns('/bar')); + }); + + test('succeedsIfTargetIsLinkLoop', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.link(ns('/bar')).createSync(ns('/foo')); + expect(l.targetSync(), ns('/bar')); + }); + }); + + group('rename', () { + test('returnsCovariantType', () async { + Link l() => fs.link(ns('/foo'))..createSync(ns('/bar')); + expect(l().renameSync(ns('/bar')), isLink); + expect(await l().rename(ns('/bar')), isLink); + }); + + test('throwsIfSourceDoesntExistAtTail', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.link(ns('/foo')).renameSync(ns('/bar')); + }); + }); + + test('throwsIfSourceDoesntExistViaTraversal', () { + expectFileSystemException(ErrorCodes.ENOENT, () { + fs.link(ns('/foo/bar')).renameSync(ns('/bar')); + }); + }); + + test('throwsIfSourceIsFile', () { + fs.file(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EINVAL, () { + fs.link(ns('/foo')).renameSync(ns('/bar')); + }); + }); + + test('throwsIfSourceIsDirectory', () { + fs.directory(ns('/foo')).createSync(); + expectFileSystemException(ErrorCodes.EISDIR, () { + fs.link(ns('/foo')).renameSync(ns('/bar')); + }); + }); + + test('succeedsIfSourceIsLinkToFile', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.file(ns('/bar')).createSync(); + Link renamed = l.renameSync(ns('/baz')); + expect(renamed.path, ns('/baz')); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.notFound); + expect(fs.typeSync(ns('/bar'), followLinks: false), + FileSystemEntityType.file); + expect(fs.typeSync(ns('/baz'), followLinks: false), + FileSystemEntityType.link); + expect(fs.link(ns('/baz')).targetSync(), ns('/bar')); + }); + + test('succeedsIfSourceIsLinkToNotFound', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + Link renamed = l.renameSync(ns('/baz')); + expect(renamed.path, ns('/baz')); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.notFound); + expect(fs.typeSync(ns('/baz'), followLinks: false), + FileSystemEntityType.link); + expect(fs.link(ns('/baz')).targetSync(), ns('/bar')); + }); + + test('succeedsIfSourceIsLinkToDirectory', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.directory(ns('/bar')).createSync(); + Link renamed = l.renameSync(ns('/baz')); + expect(renamed.path, ns('/baz')); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.notFound); + expect(fs.typeSync(ns('/bar'), followLinks: false), + FileSystemEntityType.directory); + expect(fs.typeSync(ns('/baz'), followLinks: false), + FileSystemEntityType.link); + expect(fs.link(ns('/baz')).targetSync(), ns('/bar')); + }); + + test('succeedsIfSourceIsLinkLoop', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.link(ns('/bar')).createSync(ns('/foo')); + Link renamed = l.renameSync(ns('/baz')); + expect(renamed.path, ns('/baz')); + expect(fs.typeSync(ns('/foo'), followLinks: false), + FileSystemEntityType.notFound); + expect(fs.typeSync(ns('/bar'), followLinks: false), + FileSystemEntityType.link); + expect(fs.typeSync(ns('/baz'), followLinks: false), + FileSystemEntityType.link); + expect(fs.link(ns('/baz')).targetSync(), ns('/bar')); + }); + + test('succeedsIfDestinationDoesntExistAtTail', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + Link renamed = l.renameSync(ns('/baz')); + expect(renamed.path, ns('/baz')); + expect(fs.link(ns('/foo')), isNot(exists)); + expect(fs.link(ns('/baz')), exists); + }); + + test('throwsIfDestinationDoesntExistViaTraversal', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + expectFileSystemException(ErrorCodes.ENOENT, () { + l.renameSync(ns('/baz/qux')); + }); + }); + + test('throwsIfDestinationExistsAsFile', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.file(ns('/baz')).createSync(); + expectFileSystemException(ErrorCodes.EINVAL, () { + l.renameSync(ns('/baz')); + }); + }); + + test('throwsIfDestinationExistsAsDirectory', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.directory(ns('/baz')).createSync(); + expectFileSystemException(ErrorCodes.EINVAL, () { + l.renameSync(ns('/baz')); + }); + }); + + test('succeedsIfDestinationExistsAsLinkToFile', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.file(ns('/baz')).createSync(); + fs.link(ns('/qux')).createSync(ns('/baz')); + l.renameSync(ns('/qux')); + expect(fs.typeSync(ns('/foo')), FileSystemEntityType.notFound); + expect(fs.typeSync(ns('/baz'), followLinks: false), + FileSystemEntityType.file); + expect(fs.typeSync(ns('/qux'), followLinks: false), + FileSystemEntityType.link); + expect(fs.link(ns('/qux')).targetSync(), ns('/bar')); + }); + + test('throwsIfDestinationExistsAsLinkToDirectory', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.directory(ns('/baz')).createSync(); + fs.link(ns('/qux')).createSync(ns('/baz')); + l.renameSync(ns('/qux')); + expect(fs.typeSync(ns('/foo')), FileSystemEntityType.notFound); + expect(fs.typeSync(ns('/baz'), followLinks: false), + FileSystemEntityType.directory); + expect(fs.typeSync(ns('/qux'), followLinks: false), + FileSystemEntityType.link); + expect(fs.link(ns('/qux')).targetSync(), ns('/bar')); + }); + + test('succeedsIfDestinationExistsAsLinkToNotFound', () { + Link l = fs.link(ns('/foo'))..createSync(ns('/bar')); + fs.link(ns('/baz')).createSync(ns('/qux')); + l.renameSync(ns('/baz')); + expect(fs.typeSync(ns('/foo')), FileSystemEntityType.notFound); + expect(fs.typeSync(ns('/baz'), followLinks: false), + FileSystemEntityType.link); + expect(fs.link(ns('/baz')).targetSync(), ns('/bar')); + }); + }); + }); + }); +} diff --git a/pkgs/file/test/local_test.dart b/pkgs/file/test/local_test.dart new file mode 100644 index 000000000..e1618d230 --- /dev/null +++ b/pkgs/file/test/local_test.dart @@ -0,0 +1,132 @@ +// Copyright (c) 2017, 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') +import 'dart:io' as io; + +import 'package:file/local.dart'; +import 'package:file_testing/src/testing/internal.dart'; +import 'package:test/test.dart'; + +import 'common_tests.dart'; + +void main() { + group('LocalFileSystem', () { + late LocalFileSystem fs; + late io.Directory tmp; + late String cwd; + + setUp(() { + fs = const LocalFileSystem(); + tmp = io.Directory.systemTemp.createTempSync('file_test_'); + tmp = io.Directory(tmp.resolveSymbolicLinksSync()); + cwd = io.Directory.current.path; + io.Directory.current = tmp; + }); + + tearDown(() { + io.Directory.current = cwd; + tmp.deleteSync(recursive: true); + }); + + setUpAll(() { + if (io.Platform.isWindows) { + // TODO(tvolkert): Remove once all more serious test failures are fixed + // https://github.com/google/file.dart/issues/56 + ignoreOsErrorCodes = true; + } + }); + + tearDownAll(() { + ignoreOsErrorCodes = false; + }); + + Map> skipOnPlatform = >{ + 'windows': [ + 'FileSystem > currentDirectory > throwsIfHasNonExistentPathInComplexChain', + 'FileSystem > currentDirectory > resolvesLinksIfEncountered', + 'FileSystem > currentDirectory > succeedsIfSetToDirectoryLinkAtTail', + 'FileSystem > stat > isFileForLinkToFile', + 'FileSystem > type > isFileForLinkToFileAndFollowLinksTrue', + 'FileSystem > type > isNotFoundForLinkWithCircularReferenceAndFollowLinksTrue', + 'Directory > exists > falseIfNotFoundSegmentExistsThenIsBackedOut', + 'Directory > rename > throwsIfDestinationIsNonEmptyDirectory', + 'Directory > rename > throwsIfDestinationIsLinkToEmptyDirectory', + 'Directory > resolveSymbolicLinks > throwsIfPathNotFoundInMiddleThenBackedOut', + 'Directory > resolveSymbolicLinks > handlesRelativeLinks', + 'Directory > resolveSymbolicLinks > handlesLinksWhoseTargetsHaveNestedLinks', + 'Directory > resolveSymbolicLinks > handlesComplexPathWithMultipleLinks', + 'Directory > createTemp > succeedsWithNestedPathPrefixThatExists', + 'Directory > list > followsLinksIfFollowLinksTrue', + 'Directory > list > returnsCovariantType', + 'Directory > list > returnsLinkObjectsForRecursiveLinkIfFollowLinksTrue', + 'Directory > delete > succeedsIfPathReferencesLinkToFileAndRecursiveTrue', + 'File > rename > succeedsIfSourceExistsAsLinkToFile', + 'File > copy > succeedsIfSourceExistsAsLinkToFile', + 'File > copy > succeedsIfSourceIsLinkToFileInDifferentDirectory', + 'File > delete > succeedsIfExistsAsLinkToFileAndRecursiveTrue', + 'File > openWrite > ioSink > throwsIfEncodingIsNullAndWriteObject', + 'File > openWrite > ioSink > allowsChangingEncoding', + 'File > openWrite > ioSink > succeedsIfAddRawData', + 'File > openWrite > ioSink > succeedsIfWrite', + 'File > openWrite > ioSink > succeedsIfWriteAll', + 'File > openWrite > ioSink > succeedsIfWriteCharCode', + 'File > openWrite > ioSink > succeedsIfWriteln', + 'File > openWrite > ioSink > addStream > succeedsIfStreamProducesData', + 'File > openWrite > ioSink > addStream > blocksCallToAddWhileStreamIsActive', + 'File > openWrite > ioSink > addStream > blocksCallToWriteWhileStreamIsActive', + 'File > openWrite > ioSink > addStream > blocksCallToWriteAllWhileStreamIsActive', + 'File > openWrite > ioSink > addStream > blocksCallToWriteCharCodeWhileStreamIsActive', + 'File > openWrite > ioSink > addStream > blocksCallToWritelnWhileStreamIsActive', + 'File > openWrite > ioSink > addStream > blocksCallToFlushWhileStreamIsActive', + 'File > stat > isFileIfExistsAsLinkToFile', + 'Link > stat > isFileIfTargetIsFile', + 'Link > stat > isDirectoryIfTargetIsDirectory', + 'Link > delete > unlinksIfTargetIsDirectoryAndRecursiveTrue', + 'Link > delete > unlinksIfTargetIsFileAndRecursiveTrue', + + // Fixed in SDK 1.23 (https://github.com/dart-lang/sdk/issues/28852) + 'File > open > WRITE > RandomAccessFile > truncate > throwsIfSetToNegativeNumber', + 'File > open > APPEND > RandomAccessFile > truncate > throwsIfSetToNegativeNumber', + 'File > open > WRITE_ONLY > RandomAccessFile > truncate > throwsIfSetToNegativeNumber', + 'File > open > WRITE_ONLY_APPEND > RandomAccessFile > truncate > throwsIfSetToNegativeNumber', + + // Windows does not allow removing or renaming open files. + '.* > openReadHandleDoesNotChange', + '.* > openWriteHandleDoesNotChange', + ], + }; + + runCommonTests( + () => fs, + root: () => tmp.path, + skip: [ + // https://github.com/dart-lang/sdk/issues/28171 + 'File > rename > throwsIfDestinationExistsAsLinkToDirectory', + + // https://github.com/dart-lang/sdk/issues/28275 + 'Link > rename > throwsIfDestinationExistsAsDirectory', + + // https://github.com/dart-lang/sdk/issues/28277 + 'Link > rename > throwsIfDestinationExistsAsFile', + + ...skipOnPlatform[io.Platform.operatingSystem] ?? [], + ], + ); + + group('toString', () { + test('File', () { + expect(fs.file('/foo').toString(), "LocalFile: '/foo'"); + }); + + test('Directory', () { + expect(fs.directory('/foo').toString(), "LocalDirectory: '/foo'"); + }); + + test('Link', () { + expect(fs.link('/foo').toString(), "LocalLink: '/foo'"); + }); + }); + }); +} diff --git a/pkgs/file/test/memory_operations_test.dart b/pkgs/file/test/memory_operations_test.dart new file mode 100644 index 000000000..5e27843b5 --- /dev/null +++ b/pkgs/file/test/memory_operations_test.dart @@ -0,0 +1,231 @@ +// Copyright (c) 2017, 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:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:test/test.dart'; + +void main() { + test('Read operations invoke opHandle', () async { + List contexts = []; + List operations = []; + MemoryFileSystem fs = MemoryFileSystem.test( + opHandle: (String context, FileSystemOp operation) { + if (operation == FileSystemOp.read) { + contexts.add(context); + operations.add(operation); + } + }); + final File file = fs.file('test')..createSync(); + + await file.readAsBytes(); + file.readAsBytesSync(); + await file.readAsString(); + file.readAsStringSync(); + + expect(contexts, ['test', 'test', 'test', 'test']); + expect(operations, [ + FileSystemOp.read, + FileSystemOp.read, + FileSystemOp.read, + FileSystemOp.read + ]); + }); + + test('Write operations invoke opHandle', () async { + List contexts = []; + List operations = []; + MemoryFileSystem fs = MemoryFileSystem.test( + opHandle: (String context, FileSystemOp operation) { + if (operation == FileSystemOp.write) { + contexts.add(context); + operations.add(operation); + } + }); + final File file = fs.file('test')..createSync(); + + await file.writeAsBytes([]); + file.writeAsBytesSync([]); + await file.writeAsString(''); + file.writeAsStringSync(''); + + expect(contexts, ['test', 'test', 'test', 'test']); + expect(operations, [ + FileSystemOp.write, + FileSystemOp.write, + FileSystemOp.write, + FileSystemOp.write + ]); + }); + + test('Delete operations invoke opHandle', () async { + List contexts = []; + List operations = []; + MemoryFileSystem fs = MemoryFileSystem.test( + opHandle: (String context, FileSystemOp operation) { + if (operation == FileSystemOp.delete) { + contexts.add(context); + operations.add(operation); + } + }); + final File file = fs.file('test')..createSync(); + final Directory directory = fs.directory('testDir')..createSync(); + final Link link = fs.link('testLink')..createSync('foo'); + + await file.delete(); + file.createSync(); + file.deleteSync(); + + await directory.delete(); + directory.createSync(); + directory.deleteSync(); + + await link.delete(); + link.createSync('foo'); + link.deleteSync(); + + expect(contexts, + ['test', 'test', 'testDir', 'testDir', 'testLink', 'testLink']); + expect(operations, [ + FileSystemOp.delete, + FileSystemOp.delete, + FileSystemOp.delete, + FileSystemOp.delete, + FileSystemOp.delete, + FileSystemOp.delete, + ]); + }); + + test('Create operations invoke opHandle', () async { + List contexts = []; + List operations = []; + MemoryFileSystem fs = MemoryFileSystem.test( + opHandle: (String context, FileSystemOp operation) { + if (operation == FileSystemOp.create) { + contexts.add(context); + operations.add(operation); + } + }); + fs.file('testA').createSync(); + await fs.file('testB').create(); + fs.directory('testDirA').createSync(); + await fs.directory('testDirB').create(); + fs.link('testLinkA').createSync('foo'); + await fs.link('testLinkB').create('foo'); + fs.currentDirectory.createTempSync('tmp.bar'); + await fs.currentDirectory.createTemp('tmp.bar'); + + expect(contexts, [ + 'testA', + 'testB', + 'testDirA', + 'testDirB', + 'testLinkA', + 'testLinkB', + startsWith('/tmp.bar'), + startsWith('/tmp.bar'), + ]); + expect(operations, [ + FileSystemOp.create, + FileSystemOp.create, + FileSystemOp.create, + FileSystemOp.create, + FileSystemOp.create, + FileSystemOp.create, + FileSystemOp.create, + FileSystemOp.create, + ]); + }); + + test('Open operations invoke opHandle', () async { + List contexts = []; + List operations = []; + MemoryFileSystem fs = MemoryFileSystem.test( + opHandle: (String context, FileSystemOp operation) { + if (operation == FileSystemOp.open) { + contexts.add(context); + operations.add(operation); + } + }); + final File file = fs.file('test')..createSync(); + + await file.open(); + file.openSync(); + file.openRead(); + file.openWrite(); + + expect(contexts, ['test', 'test', 'test', 'test']); + expect(operations, [ + FileSystemOp.open, + FileSystemOp.open, + FileSystemOp.open, + FileSystemOp.open, + ]); + }); + + test('Copy operations invoke opHandle', () async { + List contexts = []; + List operations = []; + MemoryFileSystem fs = MemoryFileSystem.test( + opHandle: (String context, FileSystemOp operation) { + if (operation == FileSystemOp.copy) { + contexts.add(context); + operations.add(operation); + } + }); + final File file = fs.file('test')..createSync(); + + await file.copy('A'); + file.copySync('B'); + + expect(contexts, ['test', 'test']); + expect(operations, [ + FileSystemOp.copy, + FileSystemOp.copy, + ]); + }); + + test('Exists operations invoke opHandle', () async { + List contexts = []; + List operations = []; + MemoryFileSystem fs = MemoryFileSystem.test( + opHandle: (String context, FileSystemOp operation) { + if (operation == FileSystemOp.exists) { + contexts.add(context); + operations.add(operation); + } + }); + fs.file('testA').existsSync(); + await fs.file('testB').exists(); + fs.directory('testDirA').existsSync(); + await fs.directory('testDirB').exists(); + fs.link('testLinkA').existsSync(); + await fs.link('testLinkB').exists(); + + expect(contexts, [ + 'testA', + 'testB', + 'testDirA', + 'testDirB', + 'testLinkA', + 'testLinkB', + ]); + expect(operations, [ + FileSystemOp.exists, + FileSystemOp.exists, + FileSystemOp.exists, + FileSystemOp.exists, + FileSystemOp.exists, + FileSystemOp.exists, + ]); + }); + + test('FileSystemOp toString', () { + expect(FileSystemOp.create.toString(), 'FileSystemOp.create'); + expect(FileSystemOp.delete.toString(), 'FileSystemOp.delete'); + expect(FileSystemOp.read.toString(), 'FileSystemOp.read'); + expect(FileSystemOp.write.toString(), 'FileSystemOp.write'); + expect(FileSystemOp.exists.toString(), 'FileSystemOp.exists'); + }); +} diff --git a/pkgs/file/test/memory_test.dart b/pkgs/file/test/memory_test.dart new file mode 100644 index 000000000..f3b324e6c --- /dev/null +++ b/pkgs/file/test/memory_test.dart @@ -0,0 +1,173 @@ +// Copyright (c) 2017, 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:async'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:file/src/backends/memory/memory_random_access_file.dart'; +import 'package:test/test.dart'; + +import 'common_tests.dart'; + +void main() { + group('MemoryFileSystem unix style', () { + late MemoryFileSystem fs; + + setUp(() { + fs = MemoryFileSystem(); + }); + + runCommonTests(() => fs); + + group('toString', () { + test('File', () { + expect(fs.file('/foo').toString(), "MemoryFile: '/foo'"); + }); + + test('Directory', () { + expect(fs.directory('/foo').toString(), "MemoryDirectory: '/foo'"); + }); + + test('Link', () { + expect(fs.link('/foo').toString(), "MemoryLink: '/foo'"); + }); + }); + }); + + group('MemoryFileSystem windows style', () { + late MemoryFileSystem fs; + + setUp(() { + fs = MemoryFileSystem(style: FileSystemStyle.windows); + }); + + runCommonTests( + () => fs, + root: () => fs.style.root, + ); + + group('toString', () { + test('File', () { + expect(fs.file('C:\\foo').toString(), "MemoryFile: 'C:\\foo'"); + }); + + test('Directory', () { + expect( + fs.directory('C:\\foo').toString(), "MemoryDirectory: 'C:\\foo'"); + }); + + test('Link', () { + expect(fs.link('C:\\foo').toString(), "MemoryLink: 'C:\\foo'"); + }); + }); + }); + + test('MemoryFileSystem.test', () { + final MemoryFileSystem fs = + MemoryFileSystem.test(); // creates root directory + fs.file('/test1.txt').createSync(); // creates file + fs.file('/test2.txt').createSync(); // creates file + expect(fs.directory('/').statSync().modified, DateTime(2000, 1, 1, 0, 1)); + expect( + fs.file('/test1.txt').statSync().modified, DateTime(2000, 1, 1, 0, 2)); + expect( + fs.file('/test2.txt').statSync().modified, DateTime(2000, 1, 1, 0, 3)); + fs.file('/test1.txt').createSync(); + fs.file('/test2.txt').createSync(); + expect(fs.file('/test1.txt').statSync().modified, + DateTime(2000, 1, 1, 0, 2)); // file already existed + expect(fs.file('/test2.txt').statSync().modified, + DateTime(2000, 1, 1, 0, 3)); // file already existed + fs.file('/test1.txt').writeAsStringSync('test'); // touches file + expect( + fs.file('/test1.txt').statSync().modified, DateTime(2000, 1, 1, 0, 4)); + expect(fs.file('/test2.txt').statSync().modified, + DateTime(2000, 1, 1, 0, 3)); // didn't touch it + fs.file('/test1.txt').copySync( + '/test2.txt'); // creates file, then mutates file (so time changes twice) + expect(fs.file('/test1.txt').statSync().modified, + DateTime(2000, 1, 1, 0, 4)); // didn't touch it + expect( + fs.file('/test2.txt').statSync().modified, DateTime(2000, 1, 1, 0, 6)); + }); + + test('MemoryFile.openSync returns a MemoryRandomAccessFile', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final io.File file = fs.file('/test1')..createSync(); + + io.RandomAccessFile raf = file.openSync(); + try { + expect(raf, isA()); + } finally { + raf.closeSync(); + } + + raf = await file.open(); + try { + expect(raf, isA()); + } finally { + raf.closeSync(); + } + }); + + test('MemoryFileSystem.systemTempDirectory test', () { + final MemoryFileSystem fs = MemoryFileSystem.test(); + + final io.Directory fooA = fs.systemTempDirectory.createTempSync('foo'); + final io.Directory fooB = fs.systemTempDirectory.createTempSync('foo'); + + expect(fooA.path, '/.tmp_rand0/foorand0'); + expect(fooB.path, '/.tmp_rand0/foorand1'); + + final MemoryFileSystem secondFs = MemoryFileSystem.test(); + + final io.Directory fooAA = + secondFs.systemTempDirectory.createTempSync('foo'); + final io.Directory fooBB = + secondFs.systemTempDirectory.createTempSync('foo'); + + // Names are recycled with a new instance + expect(fooAA.path, '/.tmp_rand0/foorand0'); + expect(fooBB.path, '/.tmp_rand0/foorand1'); + }); + + test('Failed UTF8 decoding in MemoryFileSystem throws a FileSystemException', + () { + final MemoryFileSystem fileSystem = MemoryFileSystem.test(); + final File file = fileSystem.file('foo') + ..writeAsBytesSync([0xFFFE]); // Invalid UTF8 + + expect(file.readAsStringSync, throwsA(isA())); + }); + + test('Creating a temporary directory actually creates the directory', () { + final MemoryFileSystem fileSystem = MemoryFileSystem.test(); + final Directory tempDir = fileSystem.currentDirectory.createTempSync('foo'); + + expect(tempDir.existsSync(), true); + }); + + test( + 'addStream forwards error to returned future and file can still be ' + 'closed', () async { + final file = MemoryFileSystem.test().file('foo').openWrite(); + await expectLater(file.addStream(Stream.error('bar')), throwsA('bar')); + await file.close(); + }); + + test( + 'addStream cancels on error and does not misbehave if the stream ' + 'produces multiple errors and then closes', () async { + final file = MemoryFileSystem.test().file('foo').openWrite(); + final controller = StreamController>() + ..addError('bar') + ..addError('baz'); + final close = controller.close(); + await expectLater(file.addStream(controller.stream), throwsA('bar')); + await file.close(); + await close; + }); +} diff --git a/pkgs/file/test/utils.dart b/pkgs/file/test/utils.dart new file mode 100644 index 000000000..231312fbe --- /dev/null +++ b/pkgs/file/test/utils.dart @@ -0,0 +1,117 @@ +// Copyright (c) 2017, 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:meta/meta.dart'; +import 'package:test/test.dart'; + +const Duration _oneSecond = Duration(seconds: 1); + +/// Returns a [DateTime] with an exact second-precision by removing the +/// milliseconds and microseconds from the specified [time]. +/// +/// If [time] is not specified, it will default to the current time. +DateTime floor([DateTime? time]) { + time ??= DateTime.now(); + return time.subtract(Duration( + milliseconds: time.millisecond, + microseconds: time.microsecond, + )); +} + +/// Returns a [DateTime] with an exact second precision, rounding up to the +/// nearest second if necessary. +/// +/// If [time] is not specified, it will default to the current time. +DateTime ceil([DateTime? time]) { + time ??= DateTime.now(); + int microseconds = (1000 * time.millisecond) + time.microsecond; + return (microseconds == 0) + ? time + // Add just enough milliseconds and microseconds to reach the next second. + : time.add(Duration(microseconds: 1000000 - microseconds)); +} + +/// Returns 1 second before the [floor] of the specified [DateTime]. +// TODO(jamesderlin): Remove this and use [floor], https://github.com/dart-lang/sdk/issues/42444 +DateTime downstairs([DateTime? time]) => floor(time).subtract(_oneSecond); + +/// Successfully matches against a [DateTime] that is the same moment or before +/// the specified [time]. +Matcher isSameOrBefore(DateTime time) => _IsSameOrBefore(time); + +/// Successfully matches against a [DateTime] that is the same moment or after +/// the specified [time]. +Matcher isSameOrAfter(DateTime time) => _IsSameOrAfter(time); + +/// Successfully matches against a [DateTime] that is after the specified +/// [time]. +Matcher isAfter(DateTime time) => _IsAfter(time); + +abstract class _CompareDateTime extends Matcher { + const _CompareDateTime(this._time, this._matcher); + + final DateTime _time; + final Matcher _matcher; + + @override + bool matches(dynamic item, Map matchState) { + return item is DateTime && + _matcher.matches(item.compareTo(_time), {}); + } + + @protected + String get descriptionOperator; + + @override + Description describe(Description description) => + description.add('a DateTime $descriptionOperator $_time'); + + @protected + String get mismatchAdjective; + + @override + Description describeMismatch( + dynamic item, + Description description, + Map matchState, + bool verbose, + ) { + if (item is DateTime) { + Duration diff = item.difference(_time).abs(); + return description.add('is $mismatchAdjective $_time by $diff'); + } else { + return description.add('is not a DateTime'); + } + } +} + +class _IsSameOrBefore extends _CompareDateTime { + const _IsSameOrBefore(DateTime time) : super(time, isNonPositive); + + @override + String get descriptionOperator => '<='; + + @override + String get mismatchAdjective => 'after'; +} + +class _IsSameOrAfter extends _CompareDateTime { + const _IsSameOrAfter(DateTime time) : super(time, isNonNegative); + + @override + String get descriptionOperator => '>='; + + @override + String get mismatchAdjective => 'before'; +} + +class _IsAfter extends _CompareDateTime { + const _IsAfter(DateTime time) : super(time, isPositive); + + @override + String get descriptionOperator => '>'; + + @override + String get mismatchAdjective => 'before'; +} diff --git a/pkgs/file/test/utils_test.dart b/pkgs/file/test/utils_test.dart new file mode 100644 index 000000000..75293bf3c --- /dev/null +++ b/pkgs/file/test/utils_test.dart @@ -0,0 +1,43 @@ +// Copyright (c) 2017, 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:test/test.dart'; + +import 'utils.dart'; + +void main() { + test('floorAndCeilProduceExactSecondDateTime', () { + DateTime time = DateTime.fromMicrosecondsSinceEpoch(1001); + DateTime lower = floor(time); + DateTime upper = ceil(time); + expect(lower.millisecond, 0); + expect(upper.millisecond, 0); + expect(lower.microsecond, 0); + expect(upper.microsecond, 0); + }); + + test('floorAndCeilWorkWithNow', () { + DateTime time = DateTime.now(); + int lower = time.difference(floor(time)).inMicroseconds; + int upper = ceil(time).difference(time).inMicroseconds; + expect(lower, lessThan(1000000)); + expect(upper, lessThanOrEqualTo(1000000)); + }); + + test('floorAndCeilWorkWithExactSecondDateTime', () { + DateTime time = DateTime.parse('1999-12-31 23:59:59'); + DateTime lower = floor(time); + DateTime upper = ceil(time); + expect(lower, time); + expect(upper, time); + }); + + test('floorAndCeilWorkWithInexactSecondDateTime', () { + DateTime time = DateTime.parse('1999-12-31 23:59:59.500'); + DateTime lower = floor(time); + DateTime upper = ceil(time); + Duration difference = upper.difference(lower); + expect(difference.inMicroseconds, 1000000); + }); +} diff --git a/pkgs/file_testing/.gitignore b/pkgs/file_testing/.gitignore new file mode 100644 index 000000000..ddfdca160 --- /dev/null +++ b/pkgs/file_testing/.gitignore @@ -0,0 +1,5 @@ +.dart_tool/ +.packages +.pub/ +build/ +pubspec.lock diff --git a/pkgs/file_testing/CHANGELOG.md b/pkgs/file_testing/CHANGELOG.md new file mode 100644 index 000000000..26dcb286a --- /dev/null +++ b/pkgs/file_testing/CHANGELOG.md @@ -0,0 +1,39 @@ +## 3.0.1-wip + +* Require Dart 3.0 or later. + +## 3.0.0 + +* Migrate to null safety. + +## 2.2.0 + +* Change dependency on `package:test_api` back to `package:test`. + +## 2.1.0 + +* Changed dependency on `package:test` to `package:test_api` +* Bumped Dart SDK constraint to match `package:test_api`'s requirements +* Updated style to match latest lint rules from Flutter repo. + +## 2.0.3 + +* Relaxed constraints on `package:test` + +## 2.0.2 + +* Bumped dependency on `package:test` to version 1.0 + +## 2.0.1 + +* Bumped Dart SDK constraint to allow for Dart 2 stable + +## 2.0.0 + +* Removed `record_replay_matchers.dart` from API + +## 1.0.0 + +* Moved `package:file/testing.dart` library into a dedicated package so that + libraries don't need to take on a transitive dependency on `package:test` + in order to use `package:file`. diff --git a/pkgs/file_testing/LICENSE b/pkgs/file_testing/LICENSE new file mode 100644 index 000000000..076334f7a --- /dev/null +++ b/pkgs/file_testing/LICENSE @@ -0,0 +1,26 @@ +Copyright 2017, the Dart project authors. All rights reserved. +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 Inc. 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. \ No newline at end of file diff --git a/pkgs/file_testing/README.md b/pkgs/file_testing/README.md new file mode 100644 index 000000000..0a8541bae --- /dev/null +++ b/pkgs/file_testing/README.md @@ -0,0 +1,38 @@ +[![pub package](https://img.shields.io/pub/v/file_testing.svg)](https://pub.dev/packages/file_testing) + +Testing utilities intended to work with `package:file`. + +## Features + +This package provides a series of matchers to be used in tests that work with file +system types. + +## Usage + +```dart +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:file_testing/file_testing.dart'; +import 'package:test/test.dart'; + +void main() { + MemoryFileSystem fs; + + setUp(() { + fs = MemoryFileSystem(); + fs.file('/foo').createSync(); + }); + + test('some test', () { + expectFileSystemException( + ErrorCodes.ENOENT, + () { + fs.directory('').resolveSymbolicLinksSync(); + }, + ); + expect(fs.file('/path/to/file'), isFile); + expect(fs.file('/path/to/directory'), isDirectory); + expect(fs.file('/foo'), exists); + }); +} +``` diff --git a/pkgs/file_testing/analysis_options.yaml b/pkgs/file_testing/analysis_options.yaml new file mode 100644 index 000000000..8fbd2e443 --- /dev/null +++ b/pkgs/file_testing/analysis_options.yaml @@ -0,0 +1,6 @@ +include: package:lints/recommended.yaml + +analyzer: + errors: + # Allow having TODOs in the code + todo: ignore diff --git a/pkgs/file_testing/lib/file_testing.dart b/pkgs/file_testing/lib/file_testing.dart new file mode 100644 index 000000000..35fcdac4c --- /dev/null +++ b/pkgs/file_testing/lib/file_testing.dart @@ -0,0 +1,6 @@ +// Copyright (c) 2017, 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. + +/// Matchers (from `package:test`) for use in tests that deal with file systems. +export 'src/testing/core_matchers.dart'; diff --git a/pkgs/file_testing/lib/src/testing/core_matchers.dart b/pkgs/file_testing/lib/src/testing/core_matchers.dart new file mode 100644 index 000000000..f58539f19 --- /dev/null +++ b/pkgs/file_testing/lib/src/testing/core_matchers.dart @@ -0,0 +1,155 @@ +// Copyright (c) 2017, 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:io'; + +import 'package:test/test.dart'; + +import 'internal.dart'; + +/// Matcher that successfully matches against any instance of [Directory]. +const Matcher isDirectory = TypeMatcher(); + +/// Matcher that successfully matches against any instance of [File]. +const Matcher isFile = TypeMatcher(); + +/// Matcher that successfully matches against any instance of [Link]. +const Matcher isLink = TypeMatcher(); + +/// Matcher that successfully matches against any instance of +/// [FileSystemEntity]. +const Matcher isFileSystemEntity = TypeMatcher(); + +/// Matcher that successfully matches against any instance of [FileStat]. +const Matcher isFileStat = TypeMatcher(); + +/// Returns a [Matcher] that matches [path] against an entity's path. +/// +/// [path] may be a String, a predicate function, or a [Matcher]. If it is +/// a String, it will be wrapped in an equality matcher. +Matcher hasPath(dynamic path) => _HasPath(path); + +/// Returns a [Matcher] that successfully matches against an instance of +/// [FileSystemException]. +/// +/// If [osErrorCode] is specified, matches will be limited to exceptions whose +/// `osError.errorCode` also match the specified matcher. +/// +/// [osErrorCode] may be an `int`, a predicate function, or a [Matcher]. If it +/// is an `int`, it will be wrapped in an equality matcher. +Matcher isFileSystemException([dynamic osErrorCode]) => + _FileSystemException(osErrorCode); + +/// Returns a matcher that successfully matches against a future or function +/// that throws a [FileSystemException]. +/// +/// If [osErrorCode] is specified, matches will be limited to exceptions whose +/// `osError.errorCode` also match the specified matcher. +/// +/// [osErrorCode] may be an `int`, a predicate function, or a [Matcher]. If it +/// is an `int`, it will be wrapped in an equality matcher. +Matcher throwsFileSystemException([dynamic osErrorCode]) => + throwsA(isFileSystemException(osErrorCode)); + +/// Expects the specified [callback] to throw a [FileSystemException] with the +/// specified [osErrorCode] (matched against the exception's +/// `osError.errorCode`). +/// +/// [osErrorCode] may be an `int`, a predicate function, or a [Matcher]. If it +/// is an `int`, it will be wrapped in an equality matcher. +/// +/// See also: +/// - [ErrorCodes] +void expectFileSystemException(dynamic osErrorCode, void Function() callback) { + expect(callback, throwsFileSystemException(osErrorCode)); +} + +/// Matcher that successfully matches against a [FileSystemEntity] that +/// exists ([FileSystemEntity.existsSync] returns true). +const Matcher exists = _Exists(); + +class _FileSystemException extends Matcher { + _FileSystemException(dynamic osErrorCode) + : _matcher = _wrapMatcher(osErrorCode); + + final Matcher? _matcher; + + static Matcher? _wrapMatcher(dynamic osErrorCode) { + if (osErrorCode == null) { + return null; + } + return ignoreOsErrorCodes ? anything : wrapMatcher(osErrorCode); + } + + @override + bool matches(dynamic item, Map matchState) { + if (item is FileSystemException) { + return _matcher == null || + _matcher!.matches(item.osError?.errorCode, matchState); + } + return false; + } + + @override + Description describe(Description desc) { + if (_matcher == null) { + return desc.add('FileSystemException'); + } else { + desc.add('FileSystemException with osError.errorCode: '); + return _matcher!.describe(desc); + } + } +} + +class _HasPath extends Matcher { + _HasPath(dynamic path) : _matcher = wrapMatcher(path); + + final Matcher _matcher; + + @override + bool matches(dynamic item, Map matchState) => + _matcher.matches(item.path, matchState); + + @override + Description describe(Description desc) { + desc.add('has path: '); + return _matcher.describe(desc); + } + + @override + Description describeMismatch( + dynamic item, + Description desc, + Map matchState, + bool verbose, + ) { + desc.add('has path: \'${item.path}\'').add('\n Which: '); + final Description pathDesc = StringDescription(); + _matcher.describeMismatch(item.path, pathDesc, matchState, verbose); + desc.add(pathDesc.toString()); + return desc; + } +} + +class _Exists extends Matcher { + const _Exists(); + + @override + bool matches(dynamic item, Map matchState) => + item is FileSystemEntity && item.existsSync(); + + @override + Description describe(Description description) => + description.add('a file system entity that exists'); + + @override + Description describeMismatch( + dynamic item, + Description description, + Map matchState, + bool verbose, + ) { + return description.add('does not exist'); + } +} diff --git a/pkgs/file_testing/lib/src/testing/internal.dart b/pkgs/file_testing/lib/src/testing/internal.dart new file mode 100644 index 000000000..8f53a657e --- /dev/null +++ b/pkgs/file_testing/lib/src/testing/internal.dart @@ -0,0 +1,6 @@ +// Copyright (c) 2017, 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. + +/// True if we should ignore OS error codes in our matchers. +bool ignoreOsErrorCodes = false; diff --git a/pkgs/file_testing/pubspec.yaml b/pkgs/file_testing/pubspec.yaml new file mode 100644 index 000000000..5df7dcb1c --- /dev/null +++ b/pkgs/file_testing/pubspec.yaml @@ -0,0 +1,13 @@ +name: file_testing +version: 3.0.1-wip +description: Testing utilities for package:file. +repository: https://github.com/dart-lang/tools/tree/main/pkgs/file_testing + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + test: ^1.23.1 + +dev_dependencies: + lints: ^2.0.1