diff --git a/pkgs/io_file/lib/src/file_system.dart b/pkgs/io_file/lib/src/file_system.dart index 0a3c6834..82661a19 100644 --- a/pkgs/io_file/lib/src/file_system.dart +++ b/pkgs/io_file/lib/src/file_system.dart @@ -209,6 +209,19 @@ abstract class FileSystem { /// ``` String createTemporaryDirectory({String? parent, String? prefix}); + /// 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; + set currentDirectory(String path); + /// TODO(brianquinlan): Add an `exists` method that can determine if a file /// exists without mutating it on Windows (maybe using `FindFirstFile`?) @@ -224,19 +237,6 @@ 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; - set currentDirectory(String path); - /// Deletes the directory at the given path. /// /// If `path` is a directory but the directory is not empty, then @@ -256,6 +256,9 @@ abstract class FileSystem { /// those links are deleted but their targets are not. void removeDirectoryTree(String path); + /// Reads the entire file contents as a list of bytes. + Uint8List readAsBytes(String path); + /// Renames, and possibly moves a file system object from one path to another. /// /// If `newPath` is a relative path, it is resolved against the current @@ -274,9 +277,6 @@ abstract class FileSystem { /// fails and raises [PathExistsException]. void rename(String oldPath, String newPath); - /// Reads the entire file contents as a list of bytes. - Uint8List readAsBytes(String path); - /// Checks whether two paths refer to the same object in the file system. /// /// Throws `PathNotFoundException` if either path doesn't exist. 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 5f640de3..8824ee4c 100644 --- a/pkgs/io_file/lib/src/vm_posix_file_system.dart +++ b/pkgs/io_file/lib/src/vm_posix_file_system.dart @@ -341,24 +341,6 @@ final class PosixFileSystem extends FileSystem { } }); - @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, systemCall: 'stat', path1: path1); - } - - final stat2 = arena(); - if (libc.stat(path2.toNativeUtf8(allocator: arena).cast(), stat2) == -1) { - final errno = libc.errno; - throw _getError(errno, systemCall: 'stat', path1: path2); - } - - return (stat1.ref.st_ino == stat2.ref.st_ino) && - (stat1.ref.st_dev == stat2.ref.st_dev); - }); - @override void createDirectory(String path) => ffi.using((arena) { if (libc.mkdir( @@ -371,6 +353,22 @@ final class PosixFileSystem extends FileSystem { } }); + @override + String createTemporaryDirectory({String? parent, String? prefix}) => + ffi.using((arena) { + final directory = parent ?? temporaryDirectory; + final template = p.join(directory, '${prefix ?? ''}XXXXXX'); + + final path = libc.mkdtemp( + template.toNativeUtf8(allocator: arena).cast(), + ); + if (path == nullptr) { + final errno = libc.errno; + throw _getError(errno, systemCall: 'mkdtemp', path1: template); + } + return path.cast().toDartString(); + }); + @override set currentDirectory(String path) => ffi.using((arena) { if (libc.chdir(path.toNativeUtf8(allocator: arena).cast()) == -1) { @@ -389,22 +387,6 @@ final class PosixFileSystem extends FileSystem { return buffer.cast().toDartString(); }); - @override - String createTemporaryDirectory({String? parent, String? prefix}) => - ffi.using((arena) { - final directory = parent ?? temporaryDirectory; - final template = p.join(directory, '${prefix ?? ''}XXXXXX'); - - final path = libc.mkdtemp( - template.toNativeUtf8(allocator: arena).cast(), - ); - if (path == nullptr) { - final errno = libc.errno; - throw _getError(errno, systemCall: 'mkdtemp', path1: template); - } - return path.cast().toDartString(); - }); - @override PosixMetadata metadata(String path) => ffi.using((arena) { final stat = arena(); @@ -535,24 +517,6 @@ final class PosixFileSystem extends FileSystem { ); }); - @override - void rename(String oldPath, String newPath) => ffi.using((arena) { - // See https://pubs.opengroup.org/onlinepubs/000095399/functions/rename.html - if (libc.rename( - oldPath.toNativeUtf8(allocator: arena).cast(), - newPath.toNativeUtf8(allocator: arena).cast(), - ) != - 0) { - final errno = libc.errno; - throw _getError( - errno, - systemCall: 'rename', - path1: oldPath, - path2: newPath, - ); - } - }); - @override Uint8List readAsBytes(String path) => ffi.using((arena) { final fd = _tempFailureRetry( @@ -637,6 +601,42 @@ final class PosixFileSystem extends FileSystem { } } + @override + void rename(String oldPath, String newPath) => ffi.using((arena) { + // See https://pubs.opengroup.org/onlinepubs/000095399/functions/rename.html + if (libc.rename( + oldPath.toNativeUtf8(allocator: arena).cast(), + newPath.toNativeUtf8(allocator: arena).cast(), + ) != + 0) { + final errno = libc.errno; + throw _getError( + errno, + systemCall: 'rename', + path1: oldPath, + path2: newPath, + ); + } + }); + + @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, systemCall: 'stat', path1: path1); + } + + final stat2 = arena(); + if (libc.stat(path2.toNativeUtf8(allocator: arena).cast(), stat2) == -1) { + final errno = libc.errno; + throw _getError(errno, systemCall: 'stat', path1: path2); + } + + return (stat1.ref.st_ino == stat2.ref.st_ino) && + (stat1.ref.st_dev == stat2.ref.st_dev); + }); + @override String get temporaryDirectory => ffi.using((arena) { var env = libc.getenv('TMPDIR'.toNativeUtf8(allocator: arena).cast()); 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 8c9b74f8..0ad3c7be 100644 --- a/pkgs/io_file/lib/src/vm_windows_file_system.dart +++ b/pkgs/io_file/lib/src/vm_windows_file_system.dart @@ -480,55 +480,6 @@ final 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( - _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(); - 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); - } - } - @override void createDirectory(String path) => using((arena) { _primeGetLastError(); @@ -585,6 +536,65 @@ final class WindowsFileSystem extends FileSystem { } while (true); }); + @override + WindowsMetadata metadata(String path) => using((arena) { + _primeGetLastError(); + + final pathUtf16 = _extendedPath(path, arena); + final fileInfo = arena(); + 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(); @@ -668,6 +678,114 @@ final class WindowsFileSystem extends FileSystem { } }); + @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(); + 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(blockSize); + final bytesRead = arena(); + 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(length); + try { + return ffi.using((arena) { + final bytesRead = arena(); + 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(); @@ -688,6 +806,55 @@ final 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( + _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(); + 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. @@ -786,173 +953,6 @@ final class WindowsFileSystem extends FileSystem { } }); - @override - WindowsMetadata metadata(String path) => using((arena) { - _primeGetLastError(); - - final pathUtf16 = _extendedPath(path, arena); - final fileInfo = arena(); - 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 - 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(); - 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(blockSize); - final bytesRead = arena(); - 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(length); - try { - return ffi.using((arena) { - final bytesRead = arena(); - 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 String get temporaryDirectory { const maxLength = win32.MAX_PATH + 1;