diff --git a/pkgs/io_file/lib/io_file.dart b/pkgs/io_file/lib/io_file.dart index 39028cc0..88636fca 100644 --- a/pkgs/io_file/lib/io_file.dart +++ b/pkgs/io_file/lib/io_file.dart @@ -2,6 +2,7 @@ // 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 'src/exceptions.dart'; export 'src/file_system.dart'; export 'src/vm_file_system_property.dart' if (dart.library.html) 'src/web_file_system_property.dart'; diff --git a/pkgs/io_file/lib/posix_file_system.dart b/pkgs/io_file/lib/posix_file_system.dart index f46b8ca8..ac5ed286 100644 --- a/pkgs/io_file/lib/posix_file_system.dart +++ b/pkgs/io_file/lib/posix_file_system.dart @@ -2,5 +2,4 @@ // 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 'src/vm_posix_file_system.dart' - if (dart.library.html) 'src/web_posix_file_system.dart'; +export 'src/posix_file_system.dart'; diff --git a/pkgs/io_file/lib/src/exceptions.dart b/pkgs/io_file/lib/src/exceptions.dart new file mode 100644 index 00000000..b116082c --- /dev/null +++ b/pkgs/io_file/lib/src/exceptions.dart @@ -0,0 +1,66 @@ +// Copyright (c) 2025, 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. + +class SystemCallError { + final String systemCall; + final int errorCode; + final String message; + + const SystemCallError(this.systemCall, this.errorCode, this.message); +} + +class IOFileException implements Exception { + final String message; + + /// The file system path on which the error occurred. + /// + /// Can be `null` if the exception does not relate directly + /// to a file system path. + final String? path; + + /// The underlying OS error. + /// + /// Can be `null` if the exception is not raised due to an OS error. + final SystemCallError? systemCall; + + const IOFileException(this.message, {this.path, this.systemCall}); + + String _toStringHelper(String className) { + final sb = StringBuffer('$className: $message'); + if (path != null) { + sb.write(', path = "$path"'); + } + if (systemCall != null) { + sb.write( + ' (${systemCall!.systemCall}: ${systemCall!.message}, ' + 'errno=${systemCall!.errorCode} )', + ); + } + return sb.toString(); + } + + @override + String toString() => _toStringHelper('IOFileException'); +} + +class PathAccessException extends IOFileException { + const PathAccessException(super.message, {super.path, super.systemCall}); + + @override + String toString() => _toStringHelper('PathAccessException'); +} + +class PathExistsException extends IOFileException { + const PathExistsException(super.message, {super.path, super.systemCall}); + + @override + String toString() => _toStringHelper('PathExistsException'); +} + +class PathNotFoundException extends IOFileException { + const PathNotFoundException(super.message, {super.path, super.systemCall}); + + @override + String toString() => _toStringHelper('PathNotFoundException'); +} diff --git a/pkgs/io_file/lib/src/fake_posix_file_system.dart b/pkgs/io_file/lib/src/fake_posix_file_system.dart new file mode 100644 index 00000000..99331e01 --- /dev/null +++ b/pkgs/io_file/lib/src/fake_posix_file_system.dart @@ -0,0 +1,211 @@ +import 'dart:convert'; + +import 'dart:typed_data'; + +import 'package:path/path.dart' as p; + +import 'exceptions.dart'; +import 'file_system.dart'; +import 'posix_file_system.dart'; + +sealed class _Entity {} + +class _File extends _Entity { + _File(); +} + +class _Directory extends _Entity { + final children = {}; + + @override + String toString() => ''; +} + +class _FakeMetadata implements PosixMetadata { + @override + DateTime get access => throw UnimplementedError(); + + @override + DateTime? get creation => throw UnimplementedError(); + + @override + bool get isDirectory => this.type == FileSystemType.directory; + + @override + bool get isFile => throw UnimplementedError(); + + @override + bool? get isHidden => throw UnimplementedError(); + + @override + bool get isLink => throw UnimplementedError(); + + @override + DateTime get modification => throw UnimplementedError(); + + @override + int get size => throw UnimplementedError(); + + @override + final FileSystemType type; + + _FakeMetadata(this.type); + + @override + int get accessedTimeNanos => throw UnimplementedError(); + + @override + int? get creationTimeNanos => throw UnimplementedError(); + + @override + int get modificationTimeNanos => throw UnimplementedError(); +} + +final class FakePosixFileSystem extends PosixFileSystem { + final context = p.Context(style: p.Style.posix); + final root = _Directory(); + final tmp = _Directory(); + + FakePosixFileSystem() { + print('Reset!'); + root.children['tmp'] = tmp; + } + + _Entity? _findComponents(List components) { + _Entity e = root; + for (var child in components.skip(1)) { + print('$child => $e'); + if (e is _Directory) { + if (!e.children.containsKey(child)) return null; + e = e.children[child]!; + } else { + return null; + } + } + return e; + } + + _Entity? _findEntity(String path) { + path = context.absolute(path); + return _findComponents(p.split(path)); + } + + _Entity? _upToLast(String path) { + path = context.absolute(path); + return _findComponents(p.split(p.dirname(path))); + } + + @override + void createDirectory(String path) { + print('createDirectory($path)'); + if (_findEntity(path) != null) { + throw PathExistsException( + 'create directory failed', + path: path, + systemCall: const SystemCallError('XXX', 17, 'XXX'), + ); + } + path = context.absolute(path); + final base = context.basename(path); + + final parent = _upToLast(path); + print('parent $parent'); + if (parent is _Directory) { + parent.children[base] = _Directory(); + } else if (parent == null) { + throw PathNotFoundException( + 'create directory failed', + path: path, + systemCall: const SystemCallError('XXX', 2, 'XXX'), + ); + } else { + throw UnsupportedError(path); + } + } + + @override + String createTemporaryDirectory({String? parent, String? prefix}) { + final parentDir = parent ?? '/tmp'; + + String path = p.join(parentDir, prefix); + createDirectory(path); + return path; + } + + @override + Metadata metadata(String path) { + final e = _findEntity(path); + return _FakeMetadata(switch (e) { + _Directory() => FileSystemType.directory, + _File() => FileSystemType.file, + _ => FileSystemType.unknown, + }); + } + + @override + Uint8List readAsBytes(String path) { + // TODO: implement readAsBytes + throw UnimplementedError(); + } + + @override + void removeDirectory(String path) { + // TODO: implement removeDirectory + } + + @override + void removeDirectoryTree(String path) { + final parent = _upToLast(path); + final base = context.basename(path); + + if (parent is _Directory) { + if (parent.children.containsKey(base)) { + // Check that it is a directory. + parent.children.remove(base); + } else { + throw UnsupportedError('XXX'); + } + } else { + throw UnsupportedError('XXX'); + } + } + + @override + void rename(String oldPath, String newPath) { + // TODO: implement rename + } + + @override + bool same(String path1, String path2) { + // TODO: implement same + throw UnimplementedError(); + } + + @override + void writeAsBytes( + String path, + Uint8List data, [ + WriteMode mode = WriteMode.failExisting, + ]) { + // TODO: implement writeAsBytes + } + + @override + void writeAsString( + String path, + String contents, [ + WriteMode mode = WriteMode.failExisting, + Encoding encoding = utf8, + String? lineTerminator, + ]) { + final parent = _upToLast(path); + final base = context.basename(path); + + if (parent is _Directory) { + // Do something with the file contents. + parent.children[base] = _File(); + } else { + throw UnsupportedError('XXX'); + } + } +} diff --git a/pkgs/io_file/lib/src/file_system.dart b/pkgs/io_file/lib/src/file_system.dart index de6047d4..b89296a1 100644 --- a/pkgs/io_file/lib/src/file_system.dart +++ b/pkgs/io_file/lib/src/file_system.dart @@ -132,7 +132,7 @@ class WriteMode { /// /// TODO(brianquinlan): Far now, this class is not meant to be implemented, /// extended outside of this package. Clarify somewhere that people implementing -/// this class should reach out to be. +/// this class should reach out to me. @sealed abstract class FileSystem { /// Create a directory at the given path. diff --git a/pkgs/io_file/lib/src/vm_posix_file_system.dart b/pkgs/io_file/lib/src/native_posix_file_system.dart similarity index 86% rename from pkgs/io_file/lib/src/vm_posix_file_system.dart rename to pkgs/io_file/lib/src/native_posix_file_system.dart index d241f5f6..dbcbd665 100644 --- a/pkgs/io_file/lib/src/vm_posix_file_system.dart +++ b/pkgs/io_file/lib/src/native_posix_file_system.dart @@ -11,9 +11,11 @@ import 'dart:typed_data'; import 'package:ffi/ffi.dart' as ffi; import 'package:path/path.dart' as p; +import 'exceptions.dart'; import 'file_system.dart'; import 'internal_constants.dart'; import 'libc.dart' as libc; +import 'posix_file_system.dart'; /// The default `mode` to use with `open` calls that may create a file. const _defaultMode = 438; // => 0666 => rw-rw-rw- @@ -26,23 +28,24 @@ const _nanosecondsPerSecond = 1000000000; bool _isDotOrDotDot(Pointer s) => // ord('.') == 46 s[0] == 46 && ((s[1] == 0) || (s[1] == 46 && s[2] == 0)); -Exception _getError(int err, String message, String path) { +Exception _getError(int err, String message, String path, String systemCall) { // TODO(brianquinlan): In the long-term, do we need to avoid exceptions that // are part of `dart:io`? Can we move those exceptions into a different // namespace? - final osError = io.OSError( - libc.strerror(err).cast().toDartString(), + final systemError = SystemCallError( + systemCall, err, + libc.strerror(err).cast().toDartString(), ); if (err == libc.EPERM || err == libc.EACCES) { - return io.PathAccessException(path, osError, message); + return PathAccessException(message, path: path, systemCall: systemError); } else if (err == libc.EEXIST) { - return io.PathExistsException(path, osError, message); + return PathExistsException(message, path: path, systemCall: systemError); } else if (err == libc.ENOENT) { - return io.PathNotFoundException(path, osError, message); + return PathNotFoundException(message, path: path, systemCall: systemError); } else { - return io.FileSystemException(message, path, osError); + return IOFileException(message, path: path, systemCall: systemError); } } @@ -56,7 +59,7 @@ int _tempFailureRetry(int Function() f) { } /// Information about a directory, link, etc. stored in the [PosixFileSystem]. -final class PosixMetadata implements Metadata { +final class NativePosixMetadata implements PosixMetadata { /// The `st_mode` field of the POSIX stat struct. /// /// See [stat.h](https://pubs.opengroup.org/onlinepubs/009696799/basedefs/sys/stat.h.html) @@ -67,27 +70,13 @@ final class PosixMetadata implements Metadata { @override final int size; - /// The time that the file system object was last accessed in nanoseconds - /// since the epoch. - /// - /// Access time is updated when the object is read or modified. - /// - /// The resolution of the access time varies by platform and file system. + @override final int accessedTimeNanos; - /// The time that the file system object was created in nanoseconds since the - /// epoch. - /// - /// This will always be `null` on Android and Linux. - /// - /// The resolution of the creation time varies by platform and file system. + @override final int? creationTimeNanos; - /// The time that the file system object was last modified in nanoseconds - /// since the epoch. - /// - /// The resolution of the modification time varies by platform and file - /// system. + @override final int modificationTimeNanos; int get _fmt => mode & libc.S_IFMT; @@ -149,7 +138,7 @@ final class PosixMetadata implements Metadata { return null; } - PosixMetadata._( + NativePosixMetadata._( this.mode, this._flags, this.size, @@ -158,15 +147,15 @@ final class PosixMetadata implements Metadata { this.modificationTimeNanos, ); - /// Construct [PosixMetadata] from data returned by the `stat` system call. - factory PosixMetadata.fromFileAttributes({ + /// Construct [NativePosixMetadata] from data returned by the `stat` system call. + factory NativePosixMetadata.fromFileAttributes({ required int mode, int flags = 0, int size = 0, int accessedTimeNanos = 0, int? creationTimeNanos, int modificationTimeNanos = 0, - }) => PosixMetadata._( + }) => NativePosixMetadata._( mode, flags, size, @@ -177,7 +166,7 @@ final class PosixMetadata implements Metadata { @override bool operator ==(Object other) => - other is PosixMetadata && + other is NativePosixMetadata && mode == other.mode && _flags == other._flags && size == other.size && @@ -210,19 +199,19 @@ external int write(int fd, Pointer buf, int count); /// A [FileSystem] implementation for POSIX systems (e.g. Android, iOS, Linux, /// macOS). -final class PosixFileSystem extends FileSystem { +final class NativePosixFileSystem extends PosixFileSystem { @override bool same(String path1, String path2) => ffi.using((arena) { final stat1 = arena(); if (libc.stat(path1.toNativeUtf8(allocator: arena).cast(), stat1) == -1) { final errno = libc.errno; - throw _getError(errno, 'stat failed', path1); + throw _getError(errno, 'stat failed', path1, 'stat'); } final stat2 = arena(); if (libc.stat(path2.toNativeUtf8(allocator: arena).cast(), stat2) == -1) { final errno = libc.errno; - throw _getError(errno, 'stat failed', path2); + throw _getError(errno, 'stat failed', path2, 'stat'); } return (stat1.ref.st_ino == stat2.ref.st_ino) && @@ -237,7 +226,7 @@ final class PosixFileSystem extends FileSystem { ) == -1) { final errno = libc.errno; - throw _getError(errno, 'create directory failed', path); + throw _getError(errno, 'create directory failed', path, 'mkdir'); } }); @@ -252,21 +241,21 @@ final class PosixFileSystem extends FileSystem { ); if (path == nullptr) { final errno = libc.errno; - throw _getError(errno, 'mkdtemp failed', template); + throw _getError(errno, 'mkdtemp failed', template, 'mkdtemp'); } return path.cast().toDartString(); }); @override - PosixMetadata metadata(String path) => ffi.using((arena) { + NativePosixMetadata metadata(String path) => ffi.using((arena) { final stat = arena(); if (libc.lstat(path.toNativeUtf8(allocator: arena).cast(), stat) == -1) { final errno = libc.errno; - throw _getError(errno, 'stat failed', path); + throw _getError(errno, 'stat failed', path, 'lstat'); } - return PosixMetadata.fromFileAttributes( + return NativePosixMetadata.fromFileAttributes( mode: stat.ref.st_mode, flags: stat.ref.st_flags, size: stat.ref.st_size, @@ -291,7 +280,7 @@ final class PosixFileSystem extends FileSystem { ) == -1) { final errno = libc.errno; - throw _getError(errno, 'remove directory failed', path); + throw _getError(errno, 'remove directory failed', path, 'unlinkat'); } }); @@ -308,13 +297,13 @@ final class PosixFileSystem extends FileSystem { ); if (fd == -1) { final errno = libc.errno; - throw _getError(errno, 'openat failed', path); + throw _getError(errno, 'openat failed', path, 'openat'); } try { final dir = libc.fdopendir(fd); if (dir == nullptr) { final errno = libc.errno; - throw _getError(errno, 'fdopendir failed', path); + throw _getError(errno, 'fdopendir failed', path, 'fdopendir'); } try { // `readdir` returns `NULL` but leaves `errno` unchanged if the end of @@ -344,7 +333,7 @@ final class PosixFileSystem extends FileSystem { /// DT_UNKNOWN. if (libc.fstatat(fd, child, stat, libc.AT_SYMLINK_NOFOLLOW) == -1) { final errno = libc.errno; - throw _getError(errno, 'fstatat failed', childPath); + throw _getError(errno, 'fstatat failed', childPath, 'fstatat'); } type = stat.ref.st_mode & libc.S_IFMT == libc.S_IFDIR @@ -356,7 +345,7 @@ final class PosixFileSystem extends FileSystem { } else { if (libc.unlinkat(fd, child, 0) == -1) { final errno = libc.errno; - throw _getError(errno, 'unlinkat failed', childPath); + throw _getError(errno, 'unlinkat failed', childPath, 'unlinkat'); } } libc.errno = 0; @@ -364,11 +353,11 @@ final class PosixFileSystem extends FileSystem { } if (libc.errno != 0) { final errno = libc.errno; - throw _getError(errno, 'readdir failed', path); + throw _getError(errno, 'readdir failed', path, 'readdir'); } if (libc.unlinkat(parentfd, name.cast(), libc.AT_REMOVEDIR) == -1) { final errno = libc.errno; - throw _getError(errno, 'unlinkat failed', path); + throw _getError(errno, 'unlinkat failed', path, 'unlinkat'); } } finally { libc.closedir(dir); @@ -396,7 +385,7 @@ final class PosixFileSystem extends FileSystem { ) != 0) { final errno = libc.errno; - throw _getError(errno, 'rename failed', oldPath); + throw _getError(errno, 'rename failed', oldPath, 'rename'); } }); @@ -411,7 +400,7 @@ final class PosixFileSystem extends FileSystem { ); if (fd == -1) { final errno = libc.errno; - throw _getError(errno, 'open failed', path); + throw _getError(errno, 'open failed', path, 'open'); } try { final stat = arena(); @@ -439,7 +428,7 @@ final class PosixFileSystem extends FileSystem { switch (r) { case -1: final errno = libc.errno; - throw _getError(errno, 'read failed', path); + throw _getError(errno, 'read failed', path, 'read'); case 0: return builder.takeBytes(); default: @@ -467,7 +456,7 @@ final class PosixFileSystem extends FileSystem { switch (r) { case -1: final errno = libc.errno; - throw _getError(errno, 'read failed', path); + throw _getError(errno, 'read failed', path, 'read'); case 0: return buffer.asTypedList( bufferOffset, @@ -524,7 +513,7 @@ final class PosixFileSystem extends FileSystem { try { if (fd == -1) { final errno = libc.errno; - throw _getError(errno, 'open failed', path); + throw _getError(errno, 'open failed', path, 'open'); } ffi.using((arena) { @@ -541,7 +530,7 @@ final class PosixFileSystem extends FileSystem { ); if (w == -1) { final errno = libc.errno; - throw _getError(errno, 'write failed', path); + throw _getError(errno, 'write failed', path, 'write'); } remaining -= w; buffer += w; diff --git a/pkgs/io_file/lib/src/posix_file_system.dart b/pkgs/io_file/lib/src/posix_file_system.dart new file mode 100644 index 00000000..5ddb5f3e --- /dev/null +++ b/pkgs/io_file/lib/src/posix_file_system.dart @@ -0,0 +1,22 @@ +import '../io_file.dart'; + +abstract class PosixMetadata implements Metadata { + int get accessedTimeNanos; + + /// The time that the file system object was created in nanoseconds since the + /// epoch. + /// + /// This will always be `null` on Android and Linux. + /// + /// The resolution of the creation time varies by platform and file system. + int? get creationTimeNanos; + + /// The time that the file system object was last modified in nanoseconds + /// since the epoch. + /// + /// The resolution of the modification time varies by platform and file + /// system. + int get modificationTimeNanos; +} + +abstract class PosixFileSystem extends FileSystem {} diff --git a/pkgs/io_file/lib/src/vm_file_system_property.dart b/pkgs/io_file/lib/src/vm_file_system_property.dart index d2193a89..6fff6a0d 100644 --- a/pkgs/io_file/lib/src/vm_file_system_property.dart +++ b/pkgs/io_file/lib/src/vm_file_system_property.dart @@ -5,9 +5,9 @@ import 'dart:io'; import 'file_system.dart'; -import 'vm_posix_file_system.dart'; +import 'native_posix_file_system.dart'; import 'vm_windows_file_system.dart'; /// Return the default [FileSystem] for the current platform. FileSystem get fileSystem => - Platform.isWindows ? WindowsFileSystem() : PosixFileSystem(); + Platform.isWindows ? WindowsFileSystem() : NativePosixFileSystem(); diff --git a/pkgs/io_file/lib/src/vm_windows_file_system.dart b/pkgs/io_file/lib/src/vm_windows_file_system.dart index 6233402e..b928010a 100644 --- a/pkgs/io_file/lib/src/vm_windows_file_system.dart +++ b/pkgs/io_file/lib/src/vm_windows_file_system.dart @@ -13,6 +13,7 @@ import 'package:ffi/ffi.dart' as ffi; import 'package:path/path.dart' as p; import 'package:win32/win32.dart' as win32; +import 'exceptions.dart'; import 'file_system.dart'; import 'internal_constants.dart'; @@ -52,8 +53,19 @@ String _formatMessage(int errorCode) { } } -Exception _getError(int errorCode, String message, [String? path]) { - final osError = io.OSError(_formatMessage(errorCode), errorCode); +// FileSystemException: unable to delete directory (OS Error: no such file or directory, errno=2): /tmp/somedir +// FileSystemException: unable to delete directory, path="/tmp/somedir" (unlinkat: 2, no such file or directory) +Exception _getError( + String message, + int errorCode, + String systemCall, [ + String? path, +]) { + final systemError = SystemCallError( + systemCall, + errorCode, + _formatMessage(errorCode), + ); if (path != null) { switch (errorCode) { @@ -65,10 +77,18 @@ Exception _getError(int errorCode, String message, [String? path]) { case win32.ERROR_LOCK_VIOLATION: case win32.ERROR_NETWORK_ACCESS_DENIED: case win32.ERROR_DRIVE_LOCKED: - return io.PathAccessException(path, osError, message); + return PathAccessException( + message, + path: path, + systemCall: systemError, + ); case win32.ERROR_FILE_EXISTS: case win32.ERROR_ALREADY_EXISTS: - return io.PathExistsException(path, osError, message); + return PathExistsException( + message, + path: path, + systemCall: systemError, + ); case win32.ERROR_FILE_NOT_FOUND: case win32.ERROR_PATH_NOT_FOUND: case win32.ERROR_INVALID_DRIVE: @@ -77,12 +97,16 @@ Exception _getError(int errorCode, String message, [String? path]) { case win32.ERROR_BAD_NETPATH: case win32.ERROR_BAD_NET_NAME: case win32.ERROR_BAD_PATHNAME: - return io.PathNotFoundException(path, osError, message); + return PathNotFoundException( + message, + path: path, + systemCall: systemError, + ); default: - return io.FileSystemException(message, path, osError); + return IOFileException(message, path: path, systemCall: systemError); } } else { - return io.FileSystemException(message, path, osError); + return IOFileException(message, path: path, systemCall: systemError); } } @@ -257,7 +281,12 @@ final class WindowsFileSystem extends FileSystem { if (win32.CreateDirectory(path.toNativeUtf16(allocator: arena), nullptr) == win32.FALSE) { final errorCode = win32.GetLastError(); - throw _getError(errorCode, 'create directory failed', path); + throw _getError( + 'create directory failed', + errorCode, + 'CreateDirectory', + path, + ); } }); @@ -279,7 +308,12 @@ final class WindowsFileSystem extends FileSystem { if (win32.RemoveDirectory(path.toNativeUtf16(allocator: arena)) == win32.FALSE) { final errorCode = win32.GetLastError(); - throw _getError(errorCode, 'remove directory failed', path); + throw _getError( + 'remove directory failed', + errorCode, + 'RemoveDirectory', + path, + ); } }); @@ -297,7 +331,12 @@ final class WindowsFileSystem extends FileSystem { if (findHandle == win32.INVALID_HANDLE_VALUE) { final errorCode = win32.GetLastError(); - throw _getError(errorCode, 'FindFirstFile failed', path); + throw _getError( + 'removeDirectoryTree failed', + errorCode, + 'FindFirstFile', + path, + ); } do { @@ -316,8 +355,9 @@ final class WindowsFileSystem extends FileSystem { win32.FALSE) { final errorCode = win32.GetLastError(); throw _getError( + 'removeDirectoryTree failed', errorCode, - 'RemoveDirectory failed for link', + 'RemoveDirectory', fullPath, ); } @@ -325,7 +365,12 @@ final class WindowsFileSystem extends FileSystem { if (win32.DeleteFile(fullPath.toNativeUtf16(allocator: arena)) == win32.FALSE) { final errorCode = win32.GetLastError(); - throw _getError(errorCode, 'DeleteFile failed for link', fullPath); + throw _getError( + 'removeDirectoryTree failed', + errorCode, + 'DeleteFile', + fullPath, + ); } } } else if ((attributes & win32.FILE_ATTRIBUTE_DIRECTORY) != 0) { @@ -334,20 +379,35 @@ final class WindowsFileSystem extends FileSystem { if (win32.DeleteFile(fullPath.toNativeUtf16(allocator: arena)) == win32.FALSE) { final errorCode = win32.GetLastError(); - throw _getError(errorCode, 'DeleteFile failed', fullPath); + throw _getError( + 'removeDirectoryTree failed', + errorCode, + 'DeleteFile', + fullPath, + ); } } } while (win32.FindNextFile(findHandle, findData) != win32.FALSE); final errorCode = win32.GetLastError(); if (errorCode != win32.ERROR_NO_MORE_FILES) { - throw _getError(errorCode, 'FindNextFile failed', path); + throw _getError( + 'removeDirectoryTree failed', + errorCode, + 'FindNextFile', + path, + ); } if (win32.RemoveDirectory(path.toNativeUtf16(allocator: arena)) == win32.FALSE) { final errorCode = win32.GetLastError(); - throw _getError(errorCode, 'remove directory failed', path); + throw _getError( + 'removeDirectoryTree failed', + errorCode, + 'RemoveDirectory', + path, + ); } }); @@ -362,7 +422,7 @@ final class WindowsFileSystem extends FileSystem { ) == win32.FALSE) { final errorCode = win32.GetLastError(); - throw _getError(errorCode, 'rename failed', oldPath); + throw _getError('rename failed', errorCode, 'MoveFileEx', oldPath); } }); @@ -407,7 +467,12 @@ final class WindowsFileSystem extends FileSystem { ) == win32.FALSE) { final errorCode = win32.GetLastError(); - throw _getError(errorCode, 'set metadata failed', path); + throw _getError( + 'set metadata failed', + errorCode, + 'GetFileAttributesEx', + path, + ); } attributes = fileInfo.ref.dwFileAttributes; } else { @@ -456,7 +521,12 @@ final class WindowsFileSystem extends FileSystem { } if (win32.SetFileAttributes(nativePath, attributes) == win32.FALSE) { final errorCode = win32.GetLastError(); - throw _getError(errorCode, 'set metadata failed', path); + throw _getError( + 'set metadata failed', + errorCode, + 'SetFileAttributes', + path, + ); } }); @@ -473,7 +543,12 @@ final class WindowsFileSystem extends FileSystem { ) == win32.FALSE) { final errorCode = win32.GetLastError(); - throw _getError(errorCode, 'metadata failed', path); + throw _getError( + 'metadata failed', + errorCode, + 'GetFileAttributesEx', + path, + ); } final info = fileInfo.ref; final attributes = info.dwFileAttributes; @@ -530,7 +605,7 @@ final class WindowsFileSystem extends FileSystem { ); if (f == win32.INVALID_HANDLE_VALUE) { final errorCode = win32.GetLastError(); - throw _getError(errorCode, 'open failed', path); + throw _getError('open failed', errorCode, 'CreateFile', path); } try { // The result of `GetFileSize` is not defined for non-seeking devices @@ -574,7 +649,7 @@ final class WindowsFileSystem extends FileSystem { errorCode == win32.ERROR_SUCCESS) { return builder.takeBytes(); } - throw _getError(errorCode, 'read failed', path); + throw _getError('read failed', errorCode, 'ReadFile', path); } if (bytesRead.value == 0) { @@ -605,7 +680,7 @@ final class WindowsFileSystem extends FileSystem { ) == win32.FALSE) { final errorCode = win32.GetLastError(); - throw _getError(errorCode, 'read failed', path); + throw _getError('read failed', errorCode, 'ReadFile', path); } bufferOffset += bytesRead.value; if (bytesRead.value == 0) { @@ -651,13 +726,18 @@ final class WindowsFileSystem extends FileSystem { ); if (h == win32.INVALID_HANDLE_VALUE) { final errorCode = win32.GetLastError(); - throw _getError(errorCode, 'CreateFile failed', path); + throw _getError('same failed', errorCode, 'CreateFile', path); } try { final info = arena(); if (win32.GetFileInformationByHandle(h, info) == win32.FALSE) { final errorCode = win32.GetLastError(); - throw _getError(errorCode, 'GetFileInformationByHandle failed', path); + throw _getError( + 'same failed', + errorCode, + 'GetFileInformationByHandle', + path, + ); } return info.ref; } finally { @@ -673,7 +753,7 @@ final class WindowsFileSystem extends FileSystem { final length = win32.GetTempPath2(maxLength, buffer); if (length == 0) { final errorCode = win32.GetLastError(); - throw _getError(errorCode, 'GetTempPath failed'); + throw _getError('temporaryDirectory failed', errorCode, 'GetTempPath2'); } return p.canonicalize(buffer.toDartString()); } finally { @@ -710,7 +790,7 @@ final class WindowsFileSystem extends FileSystem { ); if (f == win32.INVALID_HANDLE_VALUE) { final errorCode = win32.GetLastError(); - throw _getError(errorCode, 'open failed', path); + throw _getError('write failed', errorCode, 'open failed', path); } try { @@ -731,7 +811,7 @@ final class WindowsFileSystem extends FileSystem { ) == win32.FALSE) { final errorCode = win32.GetLastError(); - throw _getError(errorCode, 'write failed', path); + throw _getError('write failed', errorCode, 'WriteFile', path); } remaining -= bytesWritten.value; diff --git a/pkgs/io_file/lib/src/web_file_system_property.dart b/pkgs/io_file/lib/src/web_file_system_property.dart index fd6ede7d..4ad22153 100644 --- a/pkgs/io_file/lib/src/web_file_system_property.dart +++ b/pkgs/io_file/lib/src/web_file_system_property.dart @@ -2,9 +2,10 @@ // 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 'fake_posix_file_system.dart'; import 'file_system.dart'; +FileSystem? _fileSystem; + /// Return the default [FileSystem] for the current platform. -FileSystem get fileSystem { - throw UnsupportedError('fileSystem'); -} +FileSystem get fileSystem => _fileSystem ??= FakePosixFileSystem(); diff --git a/pkgs/io_file/lib/src/web_posix_file_system.dart b/pkgs/io_file/lib/src/web_posix_file_system.dart deleted file mode 100644 index 99b2c11f..00000000 --- a/pkgs/io_file/lib/src/web_posix_file_system.dart +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) 2025, 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 'file_system.dart'; - -/// A [FileSystem] implementation for POSIX systems (e.g. Android, iOS, Linux, -/// macOS). -final class PosixFileSystem extends FileSystem { - @override - void createDirectory(String path) { - throw UnimplementedError(); - } - - @override - String createTemporaryDirectory({String? parent, String? prefix}) { - throw UnimplementedError(); - } - - @override - Metadata metadata(String path) { - throw UnimplementedError(); - } - - @override - Uint8List readAsBytes(String path) { - throw UnimplementedError(); - } - - @override - void removeDirectory(String path) { - throw UnimplementedError(); - } - - @override - void removeDirectoryTree(String path) { - throw UnimplementedError(); - } - - @override - void rename(String oldPath, String newPath) { - throw UnimplementedError(); - } - - @override - bool same(String path1, String path2) { - throw UnimplementedError(); - } - - @override - void writeAsBytes( - String path, - Uint8List data, [ - WriteMode mode = WriteMode.failExisting, - ]) { - throw UnimplementedError(); - } - - @override - void writeAsString( - String path, - String contents, [ - WriteMode mode = WriteMode.failExisting, - Encoding encoding = utf8, - String? lineTerminator, - ]) { - throw UnimplementedError(); - } -} diff --git a/pkgs/io_file/test/create_directory_test.dart b/pkgs/io_file/test/create_directory_test.dart index fb0f8672..a87c4a94 100644 --- a/pkgs/io_file/test/create_directory_test.dart +++ b/pkgs/io_file/test/create_directory_test.dart @@ -2,47 +2,47 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -@TestOn('vm') -library; - -import 'dart:io'; - +import 'package:errno/errno.dart'; import 'package:io_file/io_file.dart'; +import 'package:io_file/src/fake_posix_file_system.dart'; +import 'package:io_file/windows_file_system.dart'; import 'package:test/test.dart'; -import 'package:win32/win32.dart' as win32; import 'errors.dart' as errors; import 'test_utils.dart'; +import 'test_utils_self.dart' show SelfTestUtils; -void main() { +void testDirectory(FileSystem fs, TestUtils testUtils) { group('createDirectory', () { late String tmp; - setUp(() => tmp = createTemp('createDirectory')); + setUp(() => tmp = testUtils.createTestDirectory('createDirectory')); - tearDown(() => deleteTemp(tmp)); + tearDown(() => testUtils.deleteDirectoryTree(tmp)); //TODO(brianquinlan): test with a very long path. test('success', () { final path = '$tmp/dir'; - fileSystem.createDirectory(path); - expect(FileSystemEntity.isDirectorySync(path), isTrue); + fs.createDirectory(path); + expect(testUtils.isDir(path), isTrue); }); test('create in non-existent directory', () { final path = '$tmp/foo/dir'; expect( - () => fileSystem.createDirectory(path), + () => fs.createDirectory(path), throwsA( isA() .having((e) => e.message, 'message', 'create directory failed') .having( - (e) => e.osError?.errorCode, + (e) => e.systemCall?.errorCode, 'errorCode', - Platform.isWindows ? win32.ERROR_PATH_NOT_FOUND : errors.enoent, + fs is WindowsFileSystem + ? WindowsErrors.pathNotFound + : errors.enoent, ), ), ); @@ -50,17 +50,19 @@ void main() { test('create over existing directory', () { final path = '$tmp/dir'; - Directory(path).createSync(); + testUtils.createDirectory(path); expect( - () => fileSystem.createDirectory(path), + () => fs.createDirectory(path), throwsA( isA() .having((e) => e.message, 'message', 'create directory failed') .having( - (e) => e.osError?.errorCode, + (e) => e.systemCall?.errorCode, 'errorCode', - Platform.isWindows ? win32.ERROR_ALREADY_EXISTS : errors.eexist, + fs is WindowsFileSystem + ? WindowsErrors.alreadyExists + : errors.eexist, ), ), ); @@ -68,20 +70,33 @@ void main() { test('create over existing file', () { final path = '$tmp/file'; - File(path).createSync(); + testUtils.createTextFile(path, 'Hello World!'); expect( - () => fileSystem.createDirectory(path), + () => fs.createDirectory(path), throwsA( isA() .having((e) => e.message, 'message', 'create directory failed') .having( - (e) => e.osError?.errorCode, + (e) => e.systemCall?.errorCode, 'errorCode', - Platform.isWindows ? win32.ERROR_ALREADY_EXISTS : errors.eexist, + fs is WindowsFileSystem + ? WindowsErrors.alreadyExists + : errors.eexist, ), ), ); }); }); } + +void main() { + group('default', () { + testDirectory(fileSystem, testUtils()); + }); + + group('fake', () { + final fs = FakePosixFileSystem(); + testDirectory(fs, SelfTestUtils(fs)); + }); +} diff --git a/pkgs/io_file/test/errors.dart b/pkgs/io_file/test/errors.dart index 063f6014..e4c1b50e 100644 --- a/pkgs/io_file/test/errors.dart +++ b/pkgs/io_file/test/errors.dart @@ -2,21 +2,21 @@ // 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 'dart:io'; import 'package:errno/errno.dart'; -int get eaccess => Platform.isMacOS ? DarwinErrors.eacces : LinuxErrors.eacces; +// true -int get eexist => Platform.isMacOS ? DarwinErrors.eexist : LinuxErrors.eexist; +int get eaccess => true ? DarwinErrors.eacces : LinuxErrors.eacces; -int get eisdir => Platform.isMacOS ? DarwinErrors.eisdir : LinuxErrors.eisdir; +int get eexist => true ? DarwinErrors.eexist : LinuxErrors.eexist; -int get enoent => Platform.isMacOS ? DarwinErrors.enoent : LinuxErrors.enoent; +int get eisdir => true ? DarwinErrors.eisdir : LinuxErrors.eisdir; -int get enotdir => - Platform.isMacOS ? DarwinErrors.enotdir : LinuxErrors.enotdir; +int get enoent => true ? DarwinErrors.enoent : LinuxErrors.enoent; -int get enotempty => - Platform.isMacOS ? DarwinErrors.enotempty : LinuxErrors.enotempty; +int get enotdir => true ? DarwinErrors.enotdir : LinuxErrors.enotdir; -int get eperm => Platform.isMacOS ? DarwinErrors.eperm : LinuxErrors.eperm; +int get enotempty => true ? DarwinErrors.enotempty : LinuxErrors.enotempty; + +int get eperm => true ? DarwinErrors.eperm : LinuxErrors.eperm; diff --git a/pkgs/io_file/test/metadata_apple_test.dart b/pkgs/io_file/test/metadata_apple_test.dart index d4286d93..f60f6570 100644 --- a/pkgs/io_file/test/metadata_apple_test.dart +++ b/pkgs/io_file/test/metadata_apple_test.dart @@ -9,7 +9,7 @@ import 'dart:ffi'; import 'dart:io'; import 'package:ffi/ffi.dart'; -import 'package:io_file/src/vm_posix_file_system.dart'; +import 'package:io_file/src/native_posix_file_system.dart'; import 'package:stdlibc/stdlibc.dart' as stdlibc; import 'package:test/test.dart'; diff --git a/pkgs/io_file/test/test_utils.dart b/pkgs/io_file/test/test_utils.dart index e03dbddc..d9a7388b 100644 --- a/pkgs/io_file/test/test_utils.dart +++ b/pkgs/io_file/test/test_utils.dart @@ -2,13 +2,17 @@ // 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 'dart:math'; import 'dart:typed_data'; +export 'test_utils_self.dart' + if (dart.library.io) 'test_utils_native.dart' + show testUtils; +/* String createTemp(String testName) => Directory.systemTemp.createTempSync(testName).absolute.path; void deleteTemp(String path) => Directory(path).deleteSync(recursive: true); +*/ Uint8List randomUint8List(int length, {int? seed}) { final random = Random(seed); @@ -18,3 +22,14 @@ Uint8List randomUint8List(int length, {int? seed}) { } return l; } + +abstract interface class TestUtils { + String createTestDirectory(String testName); + void deleteDirectoryTree(String path); + + bool isDir(String path); + + void createDirectory(String path); + + void createTextFile(String path, String s); +} diff --git a/pkgs/io_file/test/test_utils_native.dart b/pkgs/io_file/test/test_utils_native.dart new file mode 100644 index 00000000..693b4c7f --- /dev/null +++ b/pkgs/io_file/test/test_utils_native.dart @@ -0,0 +1,24 @@ +import 'dart:io'; + +import 'test_utils.dart'; + +class NativeTestUtils implements TestUtils { + @override + String createTestDirectory(String testName) => + Directory.systemTemp.createTempSync(testName).absolute.path; + + @override + void deleteDirectoryTree(String path) => + Directory(path).deleteSync(recursive: true); + + @override + bool isDir(String path) => FileSystemEntity.isDirectorySync(path); + + @override + void createDirectory(String path) => Directory(path).createSync(); + + @override + void createTextFile(String path, String s) => File(path).writeAsStringSync(s); +} + +TestUtils testUtils() => NativeTestUtils(); diff --git a/pkgs/io_file/test/test_utils_self.dart b/pkgs/io_file/test/test_utils_self.dart new file mode 100644 index 00000000..fd5a806a --- /dev/null +++ b/pkgs/io_file/test/test_utils_self.dart @@ -0,0 +1,33 @@ +import 'package:io_file/io_file.dart'; + +import 'test_utils.dart'; + +class SelfTestUtils implements TestUtils { + final FileSystem fs; + + SelfTestUtils([FileSystem? fs]) : fs = fs ?? fileSystem; + + @override + void createDirectory(String path) { + fs.createDirectory(path); + } + + @override + String createTestDirectory(String testName) => + fs.createTemporaryDirectory(prefix: testName); + + @override + void createTextFile(String path, String s) { + fs.writeAsString(path, s); + } + + @override + void deleteDirectoryTree(String path) { + fs.removeDirectoryTree(path); + } + + @override + bool isDir(String path) => fs.metadata(path).isDirectory; +} + +TestUtils testUtils() => SelfTestUtils();