Skip to content

Win long path #236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 28 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6b37c56
Fix
brianquinlan May 8, 2025
d8c2b6b
Fix
brianquinlan May 9, 2025
659e1e3
Merge remote-tracking branch 'upstream/main' into win_long_path
brianquinlan May 21, 2025
98d5a20
Posix
brianquinlan May 21, 2025
c9ee321
Tewt
brianquinlan May 21, 2025
fbea453
Update current_directory_test.dart
brianquinlan May 21, 2025
fc4653d
Fixes
brianquinlan May 21, 2025
11af994
Update current_directory_test.dart
brianquinlan May 21, 2025
7ae82eb
Update current_directory_test.dart
brianquinlan May 21, 2025
0912aab
Formatting
brianquinlan May 21, 2025
542b017
Fix
brianquinlan May 21, 2025
daee833
Update vm_windows_file_system.dart
brianquinlan May 22, 2025
5a55cb4
Long path
brianquinlan May 22, 2025
a0eac60
Update create_directory_test.dart
brianquinlan May 22, 2025
46230d4
Fixes
brianquinlan May 22, 2025
a5e5f2f
Fixes
brianquinlan May 22, 2025
992689c
Update current_directory_test.dart
brianquinlan May 22, 2025
dfdafc7
Not concurrent
brianquinlan May 23, 2025
9008d6a
Update current_directory_test.dart
brianquinlan May 23, 2025
3551bfd
Update current_directory_test.dart
brianquinlan May 23, 2025
944231b
readAsBytes
brianquinlan May 23, 2025
7087dbc
Update vm_windows_file_system.dart
brianquinlan May 23, 2025
0f5161f
Set
brianquinlan May 23, 2025
6dd301c
Merge branch 'main' into win_long_path
brianquinlan May 24, 2025
c4e29ec
Fix version
brianquinlan Jun 20, 2025
38fd7ef
Merge remote-tracking branch 'upstream/main' into win_long_path
brianquinlan Jun 20, 2025
bcb59c8
Maybe works
brianquinlan Jun 21, 2025
75a127e
Fixes
brianquinlan Jun 21, 2025
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
3 changes: 3 additions & 0 deletions pkgs/io_file/constants.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
"O_TRUNC",
"O_WRONLY"
],
"<limits.h>": [
"PATH_MAX"
],
"<dirent.h>" : [
"DT_BLK",
"DT_CHR",
Expand Down
3 changes: 3 additions & 0 deletions pkgs/io_file/dart_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
concurrency: 1
reporter: compact
test-randomize-ordering-seed: random
3 changes: 3 additions & 0 deletions pkgs/io_file/lib/src/constant_bindings.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions pkgs/io_file/lib/src/constants.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions pkgs/io_file/lib/src/file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,24 @@ abstract class FileSystem {
/// written to is to attempt to open it.
Metadata metadata(String path);

/// The current
/// [working directory](https://en.wikipedia.org/wiki/Working_directory) of
/// the Dart process.
///
/// Setting the value of this field will change the working directory for
/// *all* isolates.
///
/// On Windows, unless
/// [long paths are enabled](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation),
/// the maximum length of the [currentDirectory] path is 260 characters.
String get currentDirectory {
throw UnsupportedError('currentDirectory');
}

set currentDirectory(String path) {
throw UnsupportedError('currentDirectory');
}

/// Deletes the directory at the given path.
///
/// If `path` is a directory but the directory is not empty, then
Expand Down
8 changes: 8 additions & 0 deletions pkgs/io_file/lib/src/libc_bindings.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 29 additions & 7 deletions pkgs/io_file/lib/src/vm_posix_file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const _nanosecondsPerSecond = 1000000000;
bool _isDotOrDotDot(Pointer<Char> 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]) {
// 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?
Expand All @@ -35,12 +35,16 @@ Exception _getError(int err, String message, String path) {
err,
);

if (err == libc.EPERM || err == libc.EACCES) {
return io.PathAccessException(path, osError, message);
} else if (err == libc.EEXIST) {
return io.PathExistsException(path, osError, message);
} else if (err == libc.ENOENT) {
return io.PathNotFoundException(path, osError, message);
if (path != null) {
if (err == libc.EPERM || err == libc.EACCES) {
return io.PathAccessException(path, osError, message);
} else if (err == libc.EEXIST) {
return io.PathExistsException(path, osError, message);
} else if (err == libc.ENOENT) {
return io.PathNotFoundException(path, osError, message);
} else {
return io.FileSystemException(message, path, osError);
}
} else {
return io.FileSystemException(message, path, osError);
}
Expand Down Expand Up @@ -241,6 +245,24 @@ final class PosixFileSystem extends FileSystem {
}
});

@override
set currentDirectory(String path) => ffi.using((arena) {
if (libc.chdir(path.toNativeUtf8(allocator: arena).cast()) == -1) {
final errno = libc.errno;
throw _getError(errno, 'chdir failed', path);
}
});

@override
String get currentDirectory => ffi.using((arena) {
final buffer = arena<Char>(libc.PATH_MAX);
if (libc.getcwd(buffer, libc.PATH_MAX) == nullptr) {
final errno = libc.errno;
throw _getError(errno, 'getcwd failed', null);
}
return buffer.cast<ffi.Utf8>().toDartString();
});

@override
String createTemporaryDirectory({String? parent, String? prefix}) =>
ffi.using((arena) {
Expand Down
185 changes: 138 additions & 47 deletions pkgs/io_file/lib/src/vm_windows_file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,57 @@ void _primeGetLastError() {
win32.GetLastError();
}

///
/// Pointer<Utf16> _apiPath(String path, Allocator a) =>
/// (r'\\?\' + p.canonicalize(path)).toNativeUtf16(allocator: a);

extension on Allocator {
Pointer<Utf16> duplicateUtf16(Pointer<Utf16> str) {
final s = str.cast<WChar>();
var length = 0;
while (s[length] != 0) {
length++;
}

final t = this<WChar>(length + 1);
for (var i = 0; i < length; ++i) {
t[i] = s[length];
}
t[length] = 0;
return t.cast();
}
}

/// Convert a
Pointer<Utf16> _apiPath(String path, Allocator a) {
// The obvious
var length = 256;
var utf16Path = path.toNativeUtf16(allocator: a);
do {
final buffer = win32.wsalloc(length);
try {
final result = win32.GetFullPathName(utf16Path, length, buffer, nullptr);
if (result == 0) {
final errorCode = win32.GetLastError();
throw _getError(errorCode, 'GetFullPathName failed', path);
}
if (result < length) {
final canonicalPath = a<Pointer<Utf16>>();
win32.PathAllocCanonicalize(
buffer,
win32.PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH,
canonicalPath,
);
return a.duplicateUtf16(canonicalPath.value);
} else {
length = result;
}
} finally {
win32.free(buffer);
}
} while (true);
}

String _formatMessage(int errorCode) {
final buffer = win32.wsalloc(1024);
try {
Expand Down Expand Up @@ -249,13 +300,57 @@ final class WindowsMetadata implements Metadata {
}

/// A [FileSystem] implementation for Windows systems.
final class WindowsFileSystem extends FileSystem {
base class WindowsFileSystem extends FileSystem {
@override
bool same(String path1, String path2) => using((arena) {
// Calling `GetLastError` for the first time causes the `GetLastError`
// symbol to be loaded, which resets `GetLastError`. So make a harmless
// call before the value is needed.
win32.GetLastError();

final info1 = _byHandleFileInformation(path1, arena);
final info2 = _byHandleFileInformation(path2, arena);

return info1.dwVolumeSerialNumber == info2.dwVolumeSerialNumber &&
info1.nFileIndexHigh == info2.nFileIndexHigh &&
info1.nFileIndexLow == info2.nFileIndexLow;
});

// NOTE: the return value is allocated in the given arena!
static win32.BY_HANDLE_FILE_INFORMATION _byHandleFileInformation(
String path,
ffi.Arena arena,
) {
final h = win32.CreateFile(
path.toNativeUtf16(allocator: arena),
0,
win32.FILE_SHARE_READ | win32.FILE_SHARE_WRITE | win32.FILE_SHARE_DELETE,
nullptr,
win32.OPEN_EXISTING,
win32.FILE_FLAG_BACKUP_SEMANTICS,
win32.NULL,
);
if (h == win32.INVALID_HANDLE_VALUE) {
final errorCode = win32.GetLastError();
throw _getError(errorCode, 'CreateFile failed', path);
}
try {
final info = arena<win32.BY_HANDLE_FILE_INFORMATION>();
if (win32.GetFileInformationByHandle(h, info) == win32.FALSE) {
final errorCode = win32.GetLastError();
throw _getError(errorCode, 'GetFileInformationByHandle failed', path);
}
return info.ref;
} finally {
win32.CloseHandle(h);
}
}

@override
void createDirectory(String path) => using((arena) {
_primeGetLastError();

if (win32.CreateDirectory(path.toNativeUtf16(allocator: arena), nullptr) ==
win32.FALSE) {
if (win32.CreateDirectory(_apiPath(path, arena), nullptr) == win32.FALSE) {
final errorCode = win32.GetLastError();
throw _getError(errorCode, 'create directory failed', path);
}
Expand All @@ -272,6 +367,44 @@ final class WindowsFileSystem extends FileSystem {
return path;
}

@override
set currentDirectory(String path) => using((arena) {
// XXX
// SetCurrentDirectory does not actually support paths larger than MAX_PATH,
// this limitation is due to the size of the internal buffer used for
// storing
// current directory. In Windows 10, version 1607, changes have been made
// to the OS to lift MAX_PATH limitations from file and directory management
// APIs, but both application and OS need to opt-in into new behavior.
// See https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry#enable-long-paths-in-windows-10-version-1607-and-later

_primeGetLastError();
if (win32.SetCurrentDirectory(path.toNativeUtf16()) == win32.FALSE) {
final errorCode = win32.GetLastError();
throw _getError(errorCode, 'SetCurrentDirectory failed', path);
}
});

@override
String get currentDirectory => using((arena) {
_primeGetLastError();

var length = 256;
do {
final buffer = win32.wsalloc(length);
try {
final result = win32.GetCurrentDirectory(length, buffer);
if (result < length) {
return buffer.toDartString();
} else {
length = result;
}
} finally {
win32.free(buffer);
}
} while (true);
});

@override
void removeDirectory(String path) => using((arena) {
_primeGetLastError();
Expand Down Expand Up @@ -520,7 +653,7 @@ final class WindowsFileSystem extends FileSystem {
_primeGetLastError();

final f = win32.CreateFile(
path.toNativeUtf16(),
_apiPath(path, arena),
win32.GENERIC_READ | win32.FILE_SHARE_READ,
win32.FILE_SHARE_READ | win32.FILE_SHARE_WRITE,
nullptr,
Expand Down Expand Up @@ -623,48 +756,6 @@ final class WindowsFileSystem extends FileSystem {
}
}

@override
bool same(String path1, String path2) => using((arena) {
_primeGetLastError();

final info1 = _byHandleFileInformation(path1, arena);
final info2 = _byHandleFileInformation(path2, arena);

return info1.dwVolumeSerialNumber == info2.dwVolumeSerialNumber &&
info1.nFileIndexHigh == info2.nFileIndexHigh &&
info1.nFileIndexLow == info2.nFileIndexLow;
});

// NOTE: the return value is allocated in the given arena!
static win32.BY_HANDLE_FILE_INFORMATION _byHandleFileInformation(
String path,
ffi.Arena arena,
) {
final h = win32.CreateFile(
path.toNativeUtf16(allocator: arena),
0,
win32.FILE_SHARE_READ | win32.FILE_SHARE_WRITE | win32.FILE_SHARE_DELETE,
nullptr,
win32.OPEN_EXISTING,
win32.FILE_FLAG_BACKUP_SEMANTICS,
win32.NULL,
);
if (h == win32.INVALID_HANDLE_VALUE) {
final errorCode = win32.GetLastError();
throw _getError(errorCode, 'CreateFile failed', path);
}
try {
final info = arena<win32.BY_HANDLE_FILE_INFORMATION>();
if (win32.GetFileInformationByHandle(h, info) == win32.FALSE) {
final errorCode = win32.GetLastError();
throw _getError(errorCode, 'GetFileInformationByHandle failed', path);
}
return info.ref;
} finally {
win32.CloseHandle(h);
}
}

@override
String get temporaryDirectory {
const maxLength = win32.MAX_PATH + 1;
Expand Down Expand Up @@ -698,7 +789,7 @@ final class WindowsFileSystem extends FileSystem {
};

final f = win32.CreateFile(
path.toNativeUtf16(allocator: arena),
_apiPath(path, arena),
mode == WriteMode.appendExisting
? win32.FILE_APPEND_DATA
: win32.FILE_GENERIC_WRITE,
Expand Down
2 changes: 1 addition & 1 deletion pkgs/io_file/mobile_test/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ dev_dependencies:
logging: ^1.3.0
native_toolchain_c: ^0.16.0
path: ^1.9.1
win32: ^5.13.0
win32: ^5.14.0
errno: ^1.4.1
stdlibc:
git:
Expand Down
2 changes: 1 addition & 1 deletion pkgs/io_file/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dependencies:
meta: ^1.16.0
native_toolchain_c: ^0.16.0
path: ^1.9.1
win32: ^5.13.0
win32: ^5.14.0

dev_dependencies:
args: ^2.7.0
Expand Down
Loading
Loading