diff --git a/pkgs/io_file/README.md b/pkgs/io_file/README.md index 26c44949..96289565 100644 --- a/pkgs/io_file/README.md +++ b/pkgs/io_file/README.md @@ -21,7 +21,7 @@ See | delete file | ✓ | ✓ | ✓ | ✓ | ✓ | | | | delete tree | ✓ | ✓ | ✓ | ✓ | ✓ | | | | enum dir contents | | | | | | | | -| exists | | | | | | | +| exists | ✓ | ✓ | ✓ | ✓ | ✓ | | | | get metadata (stat) | ✓ | ✓ | ✓ | ✓ | ✓ | | | | identity (same file) | ✓ | ✓ | ✓ | ✓ | ✓ | | | | open | | | | | | | diff --git a/pkgs/io_file/lib/src/file_system.dart b/pkgs/io_file/lib/src/file_system.dart index 1bebe206..96a7602d 100644 --- a/pkgs/io_file/lib/src/file_system.dart +++ b/pkgs/io_file/lib/src/file_system.dart @@ -223,6 +223,18 @@ abstract class FileSystem { String get currentDirectory; set currentDirectory(String path); + /// Checks if a file system object exists for the given path. + /// + /// Returns `true` if a file, directory, or link exists at the given path, + /// and `false` otherwise. + /// + /// If `path` is a symbolic link, `exists` returns `false` if the link is + /// broken (i.e. the target of the link does not exist). + /// + /// On Windows, calling `exists` on a named pipe may cause the server to close + /// it. + bool exists(String path); + /// TODO(brianquinlan): Add an `exists` method that can determine if a file /// exists without mutating it on Windows (maybe using `FindFirstFile`?) diff --git a/pkgs/io_file/lib/src/vm_posix_file_system.dart b/pkgs/io_file/lib/src/vm_posix_file_system.dart index 14aa7e90..48f9565d 100644 --- a/pkgs/io_file/lib/src/vm_posix_file_system.dart +++ b/pkgs/io_file/lib/src/vm_posix_file_system.dart @@ -387,6 +387,19 @@ final class PosixFileSystem extends FileSystem { return buffer.cast().toDartString(); }); + @override + bool exists(String path) => ffi.using((arena) { + final stat = arena(); + if (libc.stat(path.toNativeUtf8(allocator: arena).cast(), stat) == -1) { + if (libc.errno == libc.ENOENT) { + return false; + } + final errno = libc.errno; + throw _getError(errno, systemCall: 'lstat', path1: path); + } + return true; + }); + @override PosixMetadata metadata(String path) => ffi.using((arena) { final stat = arena(); 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 2927ad41..60a9fec3 100644 --- a/pkgs/io_file/lib/src/vm_windows_file_system.dart +++ b/pkgs/io_file/lib/src/vm_windows_file_system.dart @@ -536,6 +536,32 @@ final class WindowsFileSystem extends FileSystem { } while (true); }); + @override + bool exists(String path) => using((arena) { + _primeGetLastError(); + + final handle = win32.CreateFile( + _extendedPath(path, arena), + 0, // No access to the object itself, just its attributes. + win32.FILE_SHARE_READ | win32.FILE_SHARE_WRITE | win32.FILE_SHARE_DELETE, + nullptr, + win32.OPEN_EXISTING, + win32.FILE_FLAG_BACKUP_SEMANTICS, // Required to open directories. + win32.NULL, + ); + + if (handle == win32.INVALID_HANDLE_VALUE) { + final errorCode = win32.GetLastError(); + if (errorCode == win32.ERROR_FILE_NOT_FOUND || + errorCode == win32.ERROR_PATH_NOT_FOUND) { + return false; + } + throw _getError(errorCode, systemCall: 'CreateFile', path1: path); + } + win32.CloseHandle(handle); + return true; + }); + @override WindowsMetadata metadata(String path) => using((arena) { _primeGetLastError(); diff --git a/pkgs/io_file/lib/src/web_posix_file_system.dart b/pkgs/io_file/lib/src/web_posix_file_system.dart index e6b4148b..2ebffe1b 100644 --- a/pkgs/io_file/lib/src/web_posix_file_system.dart +++ b/pkgs/io_file/lib/src/web_posix_file_system.dart @@ -35,6 +35,11 @@ final class PosixFileSystem extends FileSystem { throw UnimplementedError(); } + @override + bool exists(String path) { + throw UnimplementedError(); + } + @override Metadata metadata(String path) { throw UnimplementedError(); diff --git a/pkgs/io_file/lib/src/web_windows_file_system.dart b/pkgs/io_file/lib/src/web_windows_file_system.dart index 80c5135a..f84000c9 100644 --- a/pkgs/io_file/lib/src/web_windows_file_system.dart +++ b/pkgs/io_file/lib/src/web_windows_file_system.dart @@ -34,6 +34,11 @@ base class WindowsFileSystem extends FileSystem { throw UnimplementedError(); } + @override + bool exists(String path) { + throw UnimplementedError(); + } + @override Metadata metadata(String path) { throw UnimplementedError(); diff --git a/pkgs/io_file/test/exists_test.dart b/pkgs/io_file/test/exists_test.dart new file mode 100644 index 00000000..96499b60 --- /dev/null +++ b/pkgs/io_file/test/exists_test.dart @@ -0,0 +1,100 @@ +// 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. + +@TestOn('vm') +library; + +import 'dart:io' as io; + +import 'package:io_file/io_file.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +import 'file_system_file_utils.dart' hide fileUtils; +import 'test_utils.dart'; + +void tests(FileUtils utils, FileSystem fs) { + late String tmp; + late String cwd; + + setUp(() { + tmp = utils.createTestDirectory('exists'); + cwd = fs.currentDirectory; + fs.currentDirectory = tmp; + }); + + tearDown(() { + fs.currentDirectory = cwd; + utils.deleteDirectoryTree(tmp); + }); + + test('file exists', () { + final path = p.join(tmp, 'file'); + utils.createTextFile(path, 'Hello World!'); + + expect(fs.exists(path), isTrue); + }); + + test('directory exists', () { + final path = p.join(tmp, 'dir'); + utils.createDirectory(path); + + expect(fs.exists(path), isTrue); + }); + + test('link to file exists', () { + final filePath = p.join(tmp, 'file'); + final linkPath = p.join(tmp, 'link'); + utils.createTextFile(filePath, 'Hello World!'); + io.Link(linkPath).createSync(filePath); + + expect(fs.exists(linkPath), isTrue); + }); + + test('absolute path, long name', () { + final path = p.join(tmp, 'f' * 255); + utils.createTextFile(path, 'Hello World'); + + expect(fs.exists(path), isTrue); + }); + + test('relative path, long name', () { + final path = 'f' * 255; + utils.createTextFile(path, 'Hello World'); + + expect(fs.exists(path), isTrue); + }); + + test('link to directory exists', () { + final dirPath = p.join(tmp, 'dir'); + final linkPath = p.join(tmp, 'link'); + utils.createDirectory(dirPath); + io.Link(linkPath).createSync(dirPath); + + expect(fs.exists(linkPath), isTrue); + }); + + test('broken link exists', () { + final linkPath = p.join(tmp, 'link'); + io.Link(linkPath).createSync('non-existent'); + + expect(fs.exists(linkPath), isFalse); + }); + + test('path does not exist', () { + final path = p.join(tmp, 'file'); + + expect(fs.exists(path), isFalse); + }); +} + +void main() { + group('exists', () { + group('dart:io verification', () => tests(fileUtils(), fileSystem)); + group( + 'self verification', + () => tests(FileSystemFileUtils(fileSystem), fileSystem), + ); + }); +}