Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkgs/io_file/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ See
| delete file | ✓ | ✓ | ✓ | ✓ | ✓ | | |
| delete tree | ✓ | ✓ | ✓ | ✓ | ✓ | | |
| enum dir contents | | | | | | | |
| exists | | | | | | |
| exists | | | | | | | |
| get metadata (stat) | ✓ | ✓ | ✓ | ✓ | ✓ | | |
| identity (same file) | ✓ | ✓ | ✓ | ✓ | ✓ | | |
| open | | | | | | |
Expand Down
12 changes: 12 additions & 0 deletions pkgs/io_file/lib/src/file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds unexpected: it is surprising that exists can cause change to any state.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exists call CreateFile, which opens the file. dart:io (sometimes) has the same behavior (and so does Python, Rust, etc.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about making exists not follow the symlink so that it doesn't need to open the file. But that does against the current behavior of dart:io (and Python and Rust...)

/// 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`?)

Expand Down
13 changes: 13 additions & 0 deletions pkgs/io_file/lib/src/vm_posix_file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,19 @@ final class PosixFileSystem extends FileSystem {
return buffer.cast<ffi.Utf8>().toDartString();
});

@override
bool exists(String path) => ffi.using((arena) {
final stat = arena<libc.Stat>();
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<libc.Stat>();
Expand Down
26 changes: 26 additions & 0 deletions pkgs/io_file/lib/src/vm_windows_file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 5 additions & 0 deletions pkgs/io_file/lib/src/web_posix_file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 5 additions & 0 deletions pkgs/io_file/lib/src/web_windows_file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
100 changes: 100 additions & 0 deletions pkgs/io_file/test/exists_test.dart
Original file line number Diff line number Diff line change
@@ -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),
);
});
}
Loading