| // 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:ffi'; |
| import 'dart:math'; |
| import 'dart:typed_data'; |
| |
| import 'package:ffi/ffi.dart'; |
| 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'; |
| |
| const _hundredsOfNanosecondsPerMicrosecond = 10; |
| |
| DateTime _fileTimeToDateTime(int t) { |
| final microseconds = t ~/ _hundredsOfNanosecondsPerMicrosecond; |
| return DateTime.utc(1601, 1, 1, 0, 0, 0, 0, microseconds); |
| } |
| |
| void _primeGetLastError() { |
| // 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(); |
| } |
| |
| extension on Allocator { |
| /// Duplicate a `LPCWSTR` into the [Allocator]'s memory pool. |
| Pointer<Utf16> duplicateUtf16(Pointer<Utf16> str) { |
| final wcharString = str.cast<WChar>(); |
| var length = 0; |
| while (wcharString[length] != 0) { |
| length++; |
| } |
| |
| final duplicateString = this<WChar>(length + 1); |
| for (var i = 0; i < length; ++i) { |
| duplicateString[i] = wcharString[i]; |
| } |
| duplicateString[length] = 0; |
| return duplicateString.cast(); |
| } |
| } |
| |
| /// Create a Windows `LPCWSTR` path from a Dart path, possibly with (`r'\\?\'`) |
| /// prepended. |
| /// |
| /// Prepending a path with `r'\\?\'` "...tells the Windows APIs to disable all |
| /// string parsing and to send the string that follows it straight to the file |
| /// system." This has several effects (for full details, see |
| /// https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file): |
| /// - the 260 character limit on paths no longer applies |
| /// - files in the Win32 device namespace (e.g. "COM1" or named pipes) are not |
| /// visible |
| /// - special characters are no longer interpreted (e.g. "./foo" would refer to |
| /// a file with that literal name) |
| /// |
| /// In order to allow paths longer than 260 character, while still allowing |
| /// virtual files to accessed, this function does not prepend `r'\\?\'` to UNC |
| /// paths (paths starting with `r'\\'`). This means that, to open "COM1", the |
| /// caller has to explicitly indicate that "COM1" is in the device namespace by |
| /// adding the `r'\\.\'` prefix, i.e. `r'\\.\COM1'`. Note that for named pipes |
| /// and serial communication ports greater than "COM9", the Windows API already |
| /// requires that the device namespace prefix be used. |
| /// |
| /// In order to continue to allow special characters in paths, this function |
| /// converts the given path to an absolute path and then canonicalizes it before |
| /// prepending `r'\\?\'`. |
| Pointer<Utf16> _extendedPath(String path, Allocator allocator) { |
| if (path.startsWith(r'\\')) { |
| return path.toNativeUtf16(allocator: allocator); |
| } |
| |
| // Optimistically assume that the full path will be only slightly longer than |
| // the given path. If that turns out to not to be enough, we will get the |
| // required size from `GetFullPathName` and try again. |
| var length = path.length + 16; |
| var utf16Path = path.toNativeUtf16(allocator: allocator); |
| 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, systemCall: 'GetFullPathName', path1: path); |
| } |
| if (result < length) { |
| final canonicalPath = allocator<Pointer<Utf16>>(); |
| win32.PathAllocCanonicalize( |
| buffer, |
| win32.PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH, |
| canonicalPath, |
| ); |
| final dup = allocator.duplicateUtf16(canonicalPath.value); |
| return dup; |
| } else { |
| length = result; |
| } |
| } finally { |
| win32.free(buffer); |
| } |
| } while (true); |
| } |
| |
| String _formatMessage(int errorCode) { |
| final buffer = win32.wsalloc(1024); |
| try { |
| final result = win32.FormatMessage( |
| win32.FORMAT_MESSAGE_FROM_SYSTEM | win32.FORMAT_MESSAGE_IGNORE_INSERTS, |
| nullptr, |
| errorCode, |
| 0, // default language |
| buffer, |
| 1024, |
| nullptr, |
| ); |
| if (result == 0) { |
| return ''; |
| } else { |
| return buffer.toDartString(); |
| } |
| } finally { |
| win32.free(buffer); |
| } |
| } |
| |
| Exception _getError( |
| int errorCode, { |
| required String systemCall, |
| String? path1, |
| String? path2, |
| }) { |
| final message = _formatMessage(errorCode); |
| |
| switch (errorCode) { |
| case win32.ERROR_DIR_NOT_EMPTY: |
| return DirectoryNotEmptyException( |
| message, |
| errorCode: errorCode, |
| path1: path1, |
| path2: path2, |
| systemCall: systemCall, |
| ); |
| case win32.ERROR_DISK_FULL: |
| return DiskFullException( |
| message, |
| errorCode: errorCode, |
| path1: path1, |
| path2: path2, |
| systemCall: systemCall, |
| ); |
| case win32.ERROR_DIRECTORY: |
| return NotADirectoryException( |
| message, |
| errorCode: errorCode, |
| path1: path1, |
| path2: path2, |
| systemCall: systemCall, |
| ); |
| case win32.ERROR_ACCESS_DENIED: |
| case win32.ERROR_CURRENT_DIRECTORY: |
| case win32.ERROR_WRITE_PROTECT: |
| case win32.ERROR_BAD_LENGTH: |
| case win32.ERROR_SHARING_VIOLATION: |
| case win32.ERROR_LOCK_VIOLATION: |
| case win32.ERROR_NETWORK_ACCESS_DENIED: |
| case win32.ERROR_DRIVE_LOCKED: |
| return PathAccessException( |
| message, |
| errorCode: errorCode, |
| path1: path1, |
| path2: path2, |
| systemCall: systemCall, |
| ); |
| case win32.ERROR_FILE_EXISTS: |
| case win32.ERROR_ALREADY_EXISTS: |
| return PathExistsException( |
| message, |
| errorCode: errorCode, |
| path1: path1, |
| path2: path2, |
| systemCall: systemCall, |
| ); |
| case win32.ERROR_FILE_NOT_FOUND: |
| case win32.ERROR_PATH_NOT_FOUND: |
| case win32.ERROR_INVALID_DRIVE: |
| case win32.ERROR_INVALID_NAME: |
| case win32.ERROR_NO_MORE_FILES: |
| case win32.ERROR_BAD_NETPATH: |
| case win32.ERROR_BAD_NET_NAME: |
| case win32.ERROR_BAD_PATHNAME: |
| return PathNotFoundException( |
| message, |
| errorCode: errorCode, |
| path1: path1, |
| path2: path2, |
| systemCall: systemCall, |
| ); |
| case win32.ERROR_TOO_MANY_OPEN_FILES: |
| return TooManyOpenFilesException( |
| message, |
| errorCode: errorCode, |
| path1: path1, |
| path2: path2, |
| systemCall: systemCall, |
| ); |
| default: |
| return IOFileException( |
| message, |
| errorCode: errorCode, |
| path1: path1, |
| path2: path2, |
| systemCall: systemCall, |
| ); |
| } |
| } |
| |
| /// File system entity data available on Windows. |
| final class WindowsMetadata implements Metadata { |
| // TODO(brianquinlan): Reoganize fields when the POSIX `metadata` is |
| // available. |
| // TODO(brianquinlan): Document the public fields. |
| |
| /// Will never have the `FILE_ATTRIBUTE_NORMAL` bit set. |
| int _attributes; |
| int _fileType; |
| |
| @override |
| FileSystemType get type { |
| if (isDirectory) { |
| return FileSystemType.directory; |
| } |
| if (isLink) { |
| return FileSystemType.link; |
| } |
| |
| if (_fileType == win32.FILE_TYPE_CHAR) { |
| return FileSystemType.character; |
| } |
| if (_fileType == win32.FILE_TYPE_DISK) { |
| return FileSystemType.file; |
| } |
| if (_fileType == win32.FILE_TYPE_PIPE) { |
| return FileSystemType.pipe; |
| } |
| return FileSystemType.unknown; |
| } |
| |
| @override |
| // On Windows, a reparse point that refers to a directory will have the |
| // `FILE_ATTRIBUTE_DIRECTORY` attribute. |
| bool get isDirectory => |
| _attributes & win32.FILE_ATTRIBUTE_DIRECTORY != 0 && !isLink; |
| |
| @override |
| bool get isFile => type == FileSystemType.file; |
| |
| @override |
| bool get isLink => _attributes & win32.FILE_ATTRIBUTE_REPARSE_POINT != 0; |
| |
| @override |
| final int size; |
| |
| bool get isReadOnly => _attributes & win32.FILE_ATTRIBUTE_READONLY != 0; |
| @override |
| bool get isHidden => _attributes & win32.FILE_ATTRIBUTE_HIDDEN != 0; |
| bool get isSystem => _attributes & win32.FILE_ATTRIBUTE_SYSTEM != 0; |
| |
| // TODO(brianquinlan): Refer to |
| // https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/windows-scripting/5tx15443(v=vs.84)?redirectedfrom=MSDN |
| bool get needsArchive => _attributes & win32.FILE_ATTRIBUTE_ARCHIVE != 0; |
| bool get isTemporary => _attributes & win32.FILE_ATTRIBUTE_TEMPORARY != 0; |
| bool get isOffline => _attributes & win32.FILE_ATTRIBUTE_OFFLINE != 0; |
| bool get isContentIndexed => |
| _attributes & win32.FILE_ATTRIBUTE_NOT_CONTENT_INDEXED == 0; |
| |
| final int creationTime100Nanos; |
| final int lastAccessTime100Nanos; |
| final int lastWriteTime100Nanos; |
| |
| @override |
| DateTime get access => _fileTimeToDateTime(lastAccessTime100Nanos); |
| |
| @override |
| DateTime get creation => _fileTimeToDateTime(creationTime100Nanos); |
| |
| @override |
| DateTime get modification => _fileTimeToDateTime(lastWriteTime100Nanos); |
| |
| WindowsMetadata._( |
| this._attributes, |
| this._fileType, |
| this.size, |
| this.creationTime100Nanos, |
| this.lastAccessTime100Nanos, |
| this.lastWriteTime100Nanos, |
| ); |
| |
| /// TODO(bquinlan): Document this constructor. |
| /// |
| /// Make sure to reference: |
| /// [File Attribute Constants](https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants) |
| factory WindowsMetadata.fromFileAttributes({ |
| int attributes = 0, |
| int fileType = 0, // FILE_TYPE_UNKNOWN |
| int size = 0, |
| int creationTime100Nanos = 0, |
| int lastAccessTime100Nanos = 0, |
| int lastWriteTime100Nanos = 0, |
| }) => WindowsMetadata._( |
| attributes == win32.FILE_ATTRIBUTE_NORMAL ? 0 : attributes, |
| fileType, |
| size, |
| creationTime100Nanos, |
| lastAccessTime100Nanos, |
| lastWriteTime100Nanos, |
| ); |
| |
| /// TODO(bquinlan): Document this constructor. |
| factory WindowsMetadata.fromLogicalProperties({ |
| FileSystemType type = FileSystemType.unknown, |
| |
| int size = 0, |
| |
| bool isReadOnly = false, |
| bool isHidden = false, |
| bool isSystem = false, |
| bool needsArchive = false, |
| bool isTemporary = false, |
| bool isOffline = false, |
| bool isContentIndexed = false, |
| |
| int creationTime100Nanos = 0, |
| int lastAccessTime100Nanos = 0, |
| int lastWriteTime100Nanos = 0, |
| }) => WindowsMetadata._( |
| (type == FileSystemType.directory ? win32.FILE_ATTRIBUTE_DIRECTORY : 0) | |
| (type == FileSystemType.link ? win32.FILE_ATTRIBUTE_REPARSE_POINT : 0) | |
| (isReadOnly ? win32.FILE_ATTRIBUTE_READONLY : 0) | |
| (isHidden ? win32.FILE_ATTRIBUTE_HIDDEN : 0) | |
| (isSystem ? win32.FILE_ATTRIBUTE_SYSTEM : 0) | |
| (needsArchive ? win32.FILE_ATTRIBUTE_ARCHIVE : 0) | |
| (isTemporary ? win32.FILE_ATTRIBUTE_TEMPORARY : 0) | |
| (isOffline ? win32.FILE_ATTRIBUTE_OFFLINE : 0) | |
| (!isContentIndexed ? win32.FILE_ATTRIBUTE_NOT_CONTENT_INDEXED : 0), |
| switch (type) { |
| FileSystemType.character => win32.FILE_TYPE_CHAR, |
| FileSystemType.file => win32.FILE_TYPE_DISK, |
| FileSystemType.pipe => win32.FILE_TYPE_PIPE, |
| _ => throw UnsupportedError('$type is not supoorted on Windows'), |
| }, |
| size, |
| creationTime100Nanos, |
| lastAccessTime100Nanos, |
| lastWriteTime100Nanos, |
| ); |
| |
| @override |
| bool operator ==(Object other) => |
| other is WindowsMetadata && |
| _attributes == other._attributes && |
| _fileType == other._fileType && |
| size == other.size && |
| creationTime100Nanos == other.creationTime100Nanos && |
| lastAccessTime100Nanos == other.lastAccessTime100Nanos && |
| lastWriteTime100Nanos == other.lastWriteTime100Nanos; |
| |
| @override |
| int get hashCode => Object.hash( |
| _attributes, |
| _fileType, |
| size, |
| isContentIndexed, |
| creationTime100Nanos, |
| lastAccessTime100Nanos, |
| lastWriteTime100Nanos, |
| ); |
| } |
| |
| /// A [FileSystem] implementation for Windows systems. |
| /// |
| /// On Windows, paths refering to objects in the |
| /// [win32 device namespace](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-device-namespaces), |
| /// such as named pipes, physical disks, and serial comnmunications ports |
| /// (e.g. 'COM1'), must be prefixed with `r'\\.\'`. For example, `'NUL'` would |
| /// be refered to by the path `r'\\.\NUL'`. |
| final class WindowsFileSystem extends FileSystem { |
| void _slowCopy( |
| String oldPath, |
| String newPath, |
| int fromHandle, |
| int toHandle, |
| Allocator arena, |
| ) { |
| final buffer = arena<Uint8>(blockSize); |
| final bytesRead = arena<win32.DWORD>(); |
| |
| while (true) { |
| if (win32.ReadFile(fromHandle, buffer, blockSize, bytesRead, nullptr) == |
| win32.FALSE) { |
| final errorCode = win32.GetLastError(); |
| // On Windows, reading from a pipe that is closed by the writer results |
| // in ERROR_BROKEN_PIPE. |
| if (errorCode == win32.ERROR_BROKEN_PIPE || |
| errorCode == win32.ERROR_SUCCESS) { |
| return; |
| } |
| throw _getError(errorCode, systemCall: 'ReadFile', path1: oldPath); |
| } |
| |
| if (bytesRead.value == 0) { |
| return; |
| } |
| |
| var writeRemaining = bytesRead.value; |
| var writeBuffer = buffer; |
| final bytesWritten = arena<win32.DWORD>(); |
| while (writeRemaining > 0) { |
| if (win32.WriteFile( |
| toHandle, |
| writeBuffer, |
| writeRemaining, |
| bytesWritten, |
| nullptr, |
| ) == |
| win32.FALSE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError(errorCode, systemCall: 'WriteFile', path1: newPath); |
| } |
| writeRemaining -= bytesWritten.value; |
| writeBuffer += bytesWritten.value; |
| } |
| } |
| } |
| |
| @override |
| void copyFile(String oldPath, String newPath) => ffi.using((arena) { |
| _primeGetLastError(); |
| |
| final oldHandle = win32.CreateFile( |
| _extendedPath(oldPath, arena), |
| win32.GENERIC_READ | win32.FILE_SHARE_READ, |
| win32.FILE_SHARE_READ | win32.FILE_SHARE_WRITE, |
| nullptr, |
| win32.OPEN_EXISTING, |
| win32.FILE_ATTRIBUTE_NORMAL, |
| 0, |
| ); |
| if (oldHandle == win32.INVALID_HANDLE_VALUE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError(errorCode, systemCall: 'CreateFile', path1: oldPath); |
| } |
| try { |
| final newHandle = win32.CreateFile( |
| _extendedPath(newPath, arena), |
| win32.FILE_GENERIC_WRITE, |
| 0, |
| nullptr, |
| win32.CREATE_NEW, |
| win32.FILE_ATTRIBUTE_NORMAL & win32.FILE_FLAG_OPEN_REPARSE_POINT, |
| 0, |
| ); |
| if (newHandle == win32.INVALID_HANDLE_VALUE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError(errorCode, systemCall: 'CreateFile', path1: newPath); |
| } |
| try { |
| _slowCopy(oldPath, newPath, oldHandle, newHandle, arena); |
| } finally { |
| win32.CloseHandle(newHandle); |
| } |
| } finally { |
| win32.CloseHandle(oldHandle); |
| } |
| }); |
| |
| @override |
| void createDirectory(String path) => using((arena) { |
| _primeGetLastError(); |
| |
| if (win32.CreateDirectory(_extendedPath(path, arena), nullptr) == |
| win32.FALSE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError(errorCode, systemCall: 'CreateDirectory', path1: path); |
| } |
| }); |
| |
| @override |
| String createTemporaryDirectory({String? parent, String? prefix}) { |
| _primeGetLastError(); |
| |
| final suffix = win32.Guid.generate().toString(); |
| final directory = parent ?? temporaryDirectory; |
| final path = p.join(directory, '${prefix ?? ''}$suffix'); |
| createDirectory(path); |
| return path; |
| } |
| |
| @override |
| set currentDirectory(String path) => using((arena) { |
| _primeGetLastError(); |
| |
| if (win32.SetCurrentDirectory(path.toNativeUtf16()) == win32.FALSE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError( |
| errorCode, |
| systemCall: 'SetCurrentDirectory', |
| path1: 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 |
| 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(); |
| |
| final pathUtf16 = _extendedPath(path, arena); |
| final fileInfo = arena<win32.WIN32_FILE_ATTRIBUTE_DATA>(); |
| if (win32.GetFileAttributesEx( |
| pathUtf16, |
| win32.GetFileExInfoStandard, |
| fileInfo, |
| ) == |
| win32.FALSE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError( |
| errorCode, |
| systemCall: 'GetFileAttributesEx', |
| path1: path, |
| ); |
| } |
| final info = fileInfo.ref; |
| final attributes = info.dwFileAttributes; |
| |
| final h = win32.CreateFile( |
| pathUtf16, |
| 0, |
| win32.FILE_SHARE_READ | win32.FILE_SHARE_WRITE | win32.FILE_SHARE_DELETE, |
| nullptr, |
| win32.OPEN_EXISTING, |
| win32.FILE_FLAG_BACKUP_SEMANTICS, |
| win32.NULL, |
| ); |
| final int fileType; |
| if (h == win32.INVALID_HANDLE_VALUE) { |
| // `CreateFile` may have modes incompatible with opening some file types. |
| fileType = win32.FILE_TYPE_UNKNOWN; |
| } else { |
| try { |
| // Returns `FILE_TYPE_UNKNOWN` on failure, which is what we want anyway. |
| fileType = win32.GetFileType(h); |
| } finally { |
| win32.CloseHandle(h); |
| } |
| } |
| return WindowsMetadata.fromFileAttributes( |
| attributes: attributes, |
| fileType: fileType, |
| size: info.nFileSizeHigh << 32 | info.nFileSizeLow, |
| creationTime100Nanos: |
| info.ftCreationTime.dwHighDateTime << 32 | |
| info.ftCreationTime.dwLowDateTime, |
| lastAccessTime100Nanos: |
| info.ftLastAccessTime.dwHighDateTime << 32 | |
| info.ftLastAccessTime.dwLowDateTime, |
| lastWriteTime100Nanos: |
| info.ftLastWriteTime.dwHighDateTime << 32 | |
| info.ftLastWriteTime.dwLowDateTime, |
| ); |
| }); |
| |
| @override |
| void removeDirectory(String path) => using((arena) { |
| _primeGetLastError(); |
| |
| if (win32.RemoveDirectory(_extendedPath(path, arena)) == win32.FALSE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError(errorCode, systemCall: 'RemoveDirectory', path1: path); |
| } |
| }); |
| |
| @override |
| void removeDirectoryTree(String path) => using((arena) { |
| _primeGetLastError(); |
| |
| path = _extendedPath(path, arena).toDartString(); |
| final findData = arena<win32.WIN32_FIND_DATA>(); |
| final searchPath = p.join(path, '*'); |
| |
| final findHandle = win32.FindFirstFile( |
| searchPath.toNativeUtf16(allocator: arena), |
| findData, |
| ); |
| |
| if (findHandle == win32.INVALID_HANDLE_VALUE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError(errorCode, systemCall: 'FindFirstFile', path1: path); |
| } |
| |
| do { |
| final childPath = findData.ref.cFileName; |
| if (childPath == '.' || childPath == '..') { |
| continue; |
| } |
| |
| final fullPath = p.join(path, childPath); |
| final attributes = findData.ref.dwFileAttributes; |
| |
| if ((attributes & win32.FILE_ATTRIBUTE_REPARSE_POINT) != 0) { |
| // Do not recurse into directory links. |
| if ((attributes & win32.FILE_ATTRIBUTE_DIRECTORY) != 0) { |
| if (win32.RemoveDirectory(fullPath.toNativeUtf16(allocator: arena)) == |
| win32.FALSE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError( |
| errorCode, |
| systemCall: 'RemoveDirectory', |
| path1: fullPath, |
| ); |
| } |
| } else { |
| if (win32.DeleteFile(fullPath.toNativeUtf16(allocator: arena)) == |
| win32.FALSE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError( |
| errorCode, |
| systemCall: 'DeleteFile', |
| path1: fullPath, |
| ); |
| } |
| } |
| } else if ((attributes & win32.FILE_ATTRIBUTE_DIRECTORY) != 0) { |
| removeDirectoryTree(fullPath); |
| } else { |
| if (win32.DeleteFile(fullPath.toNativeUtf16(allocator: arena)) == |
| win32.FALSE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError(errorCode, systemCall: 'DeleteFile', path1: fullPath); |
| } |
| } |
| } while (win32.FindNextFile(findHandle, findData) != win32.FALSE); |
| |
| final errorCode = win32.GetLastError(); |
| if (errorCode != win32.ERROR_NO_MORE_FILES) { |
| throw _getError(errorCode, systemCall: 'FindNextFile', path1: path); |
| } |
| |
| if (win32.RemoveDirectory(path.toNativeUtf16(allocator: arena)) == |
| win32.FALSE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError(errorCode, systemCall: 'RemoveDirectory', path1: path); |
| } |
| }); |
| |
| @override |
| void removeFile(String path) => using((arena) { |
| _primeGetLastError(); |
| |
| if (win32.DeleteFile(_extendedPath(path, arena)) == win32.FALSE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError(errorCode, systemCall: 'DeleteFile', path1: path); |
| } |
| }); |
| |
| @override |
| Uint8List readAsBytes(String path) => using((arena) { |
| _primeGetLastError(); |
| |
| final f = win32.CreateFile( |
| _extendedPath(path, arena), |
| win32.GENERIC_READ | win32.FILE_SHARE_READ, |
| win32.FILE_SHARE_READ | win32.FILE_SHARE_WRITE, |
| nullptr, |
| win32.OPEN_EXISTING, |
| win32.FILE_ATTRIBUTE_NORMAL, |
| 0, |
| ); |
| if (f == win32.INVALID_HANDLE_VALUE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError(errorCode, systemCall: 'CreateFile', path1: path); |
| } |
| try { |
| // The result of `GetFileSize` is not defined for non-seeking devices |
| // such as pipes. |
| if (win32.GetFileType(f) == win32.FILE_TYPE_DISK) { |
| final highFileSize = arena<win32.DWORD>(); |
| final lowFileSize = win32.GetFileSize(f, highFileSize); |
| if (lowFileSize == 0xffffffff) { |
| // Indicates an error OR that the low order word of the is actually |
| // that constant. |
| final errorCode = win32.GetLastError(); |
| if (errorCode != win32.ERROR_SUCCESS) { |
| return _readUnknownLength(path, f); |
| } |
| } |
| final fileSize = highFileSize.value << 32 | lowFileSize; |
| if (fileSize == 0) { |
| return Uint8List(0); |
| } else { |
| return _readKnownLength(path, f, fileSize); |
| } |
| } |
| return _readUnknownLength(path, f); |
| } finally { |
| win32.CloseHandle(f); |
| } |
| }); |
| |
| Uint8List _readUnknownLength(String path, int file) => ffi.using((arena) { |
| final buffer = arena<Uint8>(blockSize); |
| final bytesRead = arena<win32.DWORD>(); |
| final builder = BytesBuilder(copy: true); |
| |
| while (true) { |
| if (win32.ReadFile(file, buffer, blockSize, bytesRead, nullptr) == |
| win32.FALSE) { |
| final errorCode = win32.GetLastError(); |
| // On Windows, reading from a pipe that is closed by the writer results |
| // in ERROR_BROKEN_PIPE. |
| if (errorCode == win32.ERROR_BROKEN_PIPE || |
| errorCode == win32.ERROR_SUCCESS) { |
| return builder.takeBytes(); |
| } |
| throw _getError(errorCode, systemCall: 'ReadFile', path1: path); |
| } |
| |
| if (bytesRead.value == 0) { |
| return builder.takeBytes(); |
| } else { |
| final typed = buffer.asTypedList(bytesRead.value); |
| builder.add(typed); |
| } |
| } |
| }); |
| |
| Uint8List _readKnownLength(String path, int file, int length) { |
| // In the happy path, `buffer` will be returned to the caller as a |
| // `Uint8List`. If there in an exception, free it and rethrow the exception. |
| final buffer = ffi.malloc<Uint8>(length); |
| try { |
| return ffi.using((arena) { |
| final bytesRead = arena<win32.DWORD>(); |
| var bufferOffset = 0; |
| |
| while (bufferOffset < length) { |
| if (win32.ReadFile( |
| file, |
| (buffer + bufferOffset).cast(), |
| min(length - bufferOffset, maxReadSize), |
| bytesRead, |
| nullptr, |
| ) == |
| win32.FALSE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError(errorCode, systemCall: 'ReadFile', path1: path); |
| } |
| bufferOffset += bytesRead.value; |
| if (bytesRead.value == 0) { |
| break; |
| } |
| } |
| return buffer.asTypedList( |
| bufferOffset, |
| finalizer: ffi.malloc.nativeFree, |
| ); |
| }); |
| } on Exception { |
| ffi.malloc.free(buffer); |
| rethrow; |
| } |
| } |
| |
| @override |
| void rename(String oldPath, String newPath) => using((arena) { |
| _primeGetLastError(); |
| |
| if (win32.MoveFileEx( |
| _extendedPath(oldPath, arena), |
| _extendedPath(newPath, arena), |
| win32.MOVEFILE_WRITE_THROUGH | win32.MOVEFILE_REPLACE_EXISTING, |
| ) == |
| win32.FALSE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError( |
| errorCode, |
| systemCall: 'MoveFileEx', |
| path1: oldPath, |
| path2: newPath, |
| ); |
| } |
| }); |
| |
| @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( |
| _extendedPath(path, 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, systemCall: 'CreateFile', path1: path); |
| } |
| try { |
| final info = arena<win32.BY_HANDLE_FILE_INFORMATION>(); |
| if (win32.GetFileInformationByHandle(h, info) == win32.FALSE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError( |
| errorCode, |
| systemCall: 'GetFileInformationByHandle', |
| path1: path, |
| ); |
| } |
| return info.ref; |
| } finally { |
| win32.CloseHandle(h); |
| } |
| } |
| |
| /// Sets metadata for the file system entity. |
| /// |
| /// TODO(brianquinlan): Document the arguments. |
| /// |
| /// Make sure to document that [original] should come from a call to |
| /// `metadata`. Creating your own `WindowsMetadata` will result in unsupported |
| /// fields being cleared. |
| void setMetadata( |
| String path, { |
| bool? isReadOnly, |
| bool? isHidden, |
| bool? isSystem, |
| bool? needsArchive, |
| bool? isTemporary, |
| bool? isContentIndexed, |
| bool? isOffline, |
| WindowsMetadata? original, |
| }) => using((arena) { |
| _primeGetLastError(); |
| |
| if ((isReadOnly ?? |
| isHidden ?? |
| isSystem ?? |
| needsArchive ?? |
| isTemporary ?? |
| isContentIndexed ?? |
| isOffline) == |
| null) { |
| return; |
| } |
| final fileInfo = arena<win32.WIN32_FILE_ATTRIBUTE_DATA>(); |
| final nativePath = _extendedPath(path, arena); |
| int attributes; |
| if (original == null) { |
| if (win32.GetFileAttributesEx( |
| nativePath, |
| win32.GetFileExInfoStandard, |
| fileInfo, |
| ) == |
| win32.FALSE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError( |
| errorCode, |
| systemCall: 'GetFileAttributesEx', |
| path1: path, |
| ); |
| } |
| attributes = fileInfo.ref.dwFileAttributes; |
| } else { |
| attributes = original._attributes; |
| } |
| |
| if (attributes == win32.FILE_ATTRIBUTE_NORMAL) { |
| // `FILE_ATTRIBUTE_NORMAL` indicates that no other attributes are set and |
| // is valid only when used alone. |
| attributes = 0; |
| } |
| |
| int updateBit(int base, int value, bool? bit) => switch (bit) { |
| null => base, |
| true => base | value, |
| false => base & ~value, |
| }; |
| |
| attributes = updateBit( |
| attributes, |
| win32.FILE_ATTRIBUTE_READONLY, |
| isReadOnly, |
| ); |
| attributes = updateBit(attributes, win32.FILE_ATTRIBUTE_HIDDEN, isHidden); |
| attributes = updateBit(attributes, win32.FILE_ATTRIBUTE_SYSTEM, isSystem); |
| attributes = updateBit( |
| attributes, |
| win32.FILE_ATTRIBUTE_ARCHIVE, |
| needsArchive, |
| ); |
| attributes = updateBit( |
| attributes, |
| win32.FILE_ATTRIBUTE_TEMPORARY, |
| isTemporary, |
| ); |
| attributes = updateBit( |
| attributes, |
| win32.FILE_ATTRIBUTE_NOT_CONTENT_INDEXED, |
| isContentIndexed != null ? !isContentIndexed : null, |
| ); |
| attributes = updateBit(attributes, win32.FILE_ATTRIBUTE_OFFLINE, isOffline); |
| if (attributes == 0) { |
| // `FILE_ATTRIBUTE_NORMAL` indicates that no other attributes are set and |
| // is valid only when used alone. |
| attributes = win32.FILE_ATTRIBUTE_NORMAL; |
| } |
| if (win32.SetFileAttributes(nativePath, attributes) == win32.FALSE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError(errorCode, systemCall: 'SetFileAttributes', path1: path); |
| } |
| }); |
| |
| @override |
| String get temporaryDirectory { |
| const maxLength = win32.MAX_PATH + 1; |
| final buffer = win32.wsalloc(maxLength); |
| try { |
| final length = win32.GetTempPath2(maxLength, buffer); |
| if (length == 0) { |
| final errorCode = win32.GetLastError(); |
| throw _getError(errorCode, systemCall: 'GetTempPath2'); |
| } |
| return p.canonicalize(buffer.toDartString()); |
| } finally { |
| win32.free(buffer); |
| } |
| } |
| |
| @override |
| void writeAsBytes( |
| String path, |
| Uint8List data, [ |
| WriteMode mode = WriteMode.failExisting, |
| ]) => using((arena) { |
| _primeGetLastError(); |
| |
| var createFlags = 0; |
| createFlags |= switch (mode) { |
| WriteMode.appendExisting => win32.OPEN_ALWAYS, |
| WriteMode.failExisting => win32.CREATE_NEW, |
| WriteMode.truncateExisting => win32.CREATE_ALWAYS, |
| _ => throw ArgumentError.value(mode, 'invalid write mode'), |
| }; |
| |
| final f = win32.CreateFile( |
| _extendedPath(path, arena), |
| mode == WriteMode.appendExisting |
| ? win32.FILE_APPEND_DATA |
| : win32.FILE_GENERIC_WRITE, |
| 0, |
| nullptr, |
| createFlags, |
| win32.FILE_ATTRIBUTE_NORMAL, |
| 0, |
| ); |
| if (f == win32.INVALID_HANDLE_VALUE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError(errorCode, systemCall: 'CreateFile', path1: path); |
| } |
| |
| try { |
| // TODO(brianquinlan): Remove this copy when typed data pointers are |
| // available for non-leaf calls. |
| var buffer = arena<Uint8>(data.length); |
| buffer.asTypedList(data.length).setAll(0, data); |
| final bytesWritten = arena<win32.DWORD>(); |
| var remaining = data.length; |
| |
| while (remaining > 0) { |
| if (win32.WriteFile( |
| f, |
| buffer.cast(), |
| min(maxWriteSize, remaining), |
| bytesWritten, |
| nullptr, |
| ) == |
| win32.FALSE) { |
| final errorCode = win32.GetLastError(); |
| throw _getError(errorCode, systemCall: 'WriteFile', path1: path); |
| } |
| |
| remaining -= bytesWritten.value; |
| buffer += bytesWritten.value; |
| } |
| } finally { |
| win32.CloseHandle(f); |
| } |
| }); |
| |
| @override |
| void writeAsString( |
| String path, |
| String contents, [ |
| WriteMode mode = WriteMode.failExisting, |
| Encoding encoding = utf8, |
| String? lineTerminator, |
| ]) { |
| lineTerminator ??= '\r\n'; |
| final List<int> data; |
| if (lineTerminator == '\n') { |
| data = encoding.encode(contents); |
| } else if (lineTerminator == '\r\n') { |
| data = encoding.encode(contents.replaceAll('\n', '\r\n')); |
| } else { |
| throw ArgumentError.value(lineTerminator, 'lineTerminator'); |
| } |
| writeAsBytes( |
| path, |
| data is Uint8List ? data : Uint8List.fromList(data), |
| mode, |
| ); |
| } |
| } |