diff --git a/pkgs/io_file/example/io_file_example.dart b/pkgs/io_file/example/io_file_example.dart index 6f1e37f4..fa977684 100644 --- a/pkgs/io_file/example/io_file_example.dart +++ b/pkgs/io_file/example/io_file_example.dart @@ -2,9 +2,25 @@ // 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 'package:io_file/posix_file_system.dart'; +import 'package:io_file/io_file.dart'; +import 'package:io_file/windows_file_system.dart'; + +import 'package:path/path.dart' as p; + +bool isHidden(String path) { + if (fileSystem case final WindowsFileSystem fs) { + return fs.metadata(path).isHidden; + } else { + // On POSIX, convention is that files and directories starting with a period + // are hidden (except for the special files representing the current working + // directory and parent directory). + // + // In addition, macOS has the UF_HIDDEN flag. + final name = p.basename(path); + return name.startsWith('.') && name != '.' && name != '..'; + } +} void main() { - // TODO(brianquinlan): Create a better example. - PosixFileSystem().rename('foo.txt', 'bar.txt'); + isHidden('somefile'); } diff --git a/pkgs/io_file/lib/src/file_system.dart b/pkgs/io_file/lib/src/file_system.dart index 003e909c..a9db8344 100644 --- a/pkgs/io_file/lib/src/file_system.dart +++ b/pkgs/io_file/lib/src/file_system.dart @@ -4,6 +4,16 @@ import 'dart:typed_data'; +/// Information about a directory, link, etc. stored in the [FileSystem]. +abstract interface class Metadata { + // TODO(brianquinlan): Document all public fields. + + bool get isFile; + bool get isDirectory; + bool get isLink; + int get size; +} + /// The modes in which a File can be written. class WriteMode { /// Open the file for writing such that data can only be appended to the end @@ -29,7 +39,7 @@ class WriteMode { } /// An abstract representation of a file system. -base class FileSystem { +abstract base class FileSystem { /// 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 @@ -50,6 +60,14 @@ base class FileSystem { throw UnsupportedError('rename'); } + /// Metadata for the file system object at [path]. + /// + /// If `path` represents a symbolic link then metadata for the link is + /// returned. + Metadata metadata(String path) { + throw UnsupportedError('metadata'); + } + /// Reads the entire file contents as a list of bytes. Uint8List readAsBytes(String path) { throw UnsupportedError('readAsBytes'); 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 802859c7..6f7acfad 100644 --- a/pkgs/io_file/lib/src/vm_windows_file_system.dart +++ b/pkgs/io_file/lib/src/vm_windows_file_system.dart @@ -14,6 +14,13 @@ import 'package:win32/win32.dart' as win32; 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); +} + String _formatMessage(int errorCode) { final buffer = win32.wsalloc(1024); try { @@ -66,14 +73,140 @@ Exception _getError(int errorCode, String message, String path) { } } +/// 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; + + @override + bool get isDirectory => _attributes & win32.FILE_ATTRIBUTE_DIRECTORY != 0; + + @override + bool get isFile => !isDirectory && !isLink; + + @override + bool get isLink => _attributes & win32.FILE_ATTRIBUTE_REPARSE_POINT != 0; + + @override + final int size; + + bool get isReadOnly => _attributes & win32.FILE_ATTRIBUTE_READONLY != 0; + 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; + + DateTime get creation => _fileTimeToDateTime(creationTime100Nanos); + DateTime get access => _fileTimeToDateTime(lastAccessTime100Nanos); + DateTime get modification => _fileTimeToDateTime(lastWriteTime100Nanos); + + WindowsMetadata._( + this._attributes, + 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 size = 0, + int creationTime100Nanos = 0, + int lastAccessTime100Nanos = 0, + int lastWriteTime100Nanos = 0, + }) => WindowsMetadata._( + attributes == win32.FILE_ATTRIBUTE_NORMAL ? 0 : attributes, + size, + creationTime100Nanos, + lastAccessTime100Nanos, + lastWriteTime100Nanos, + ); + + /// TODO(bquinlan): Document this constructor. + factory WindowsMetadata.fromLogicalProperties({ + bool isDirectory = false, + bool isLink = false, + + 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._( + (isDirectory ? win32.FILE_ATTRIBUTE_DIRECTORY : 0) | + (isLink ? 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), + size, + creationTime100Nanos, + lastAccessTime100Nanos, + lastWriteTime100Nanos, + ); + + @override + bool operator ==(Object other) => + other is WindowsMetadata && + _attributes == other._attributes && + size == other.size && + creationTime100Nanos == other.creationTime100Nanos && + lastAccessTime100Nanos == other.lastAccessTime100Nanos && + lastWriteTime100Nanos == other.lastWriteTime100Nanos; + + @override + int get hashCode => Object.hash( + _attributes, + size, + isContentIndexed, + creationTime100Nanos, + lastAccessTime100Nanos, + lastWriteTime100Nanos, + ); +} + /// A [FileSystem] implementation for Windows systems. base class WindowsFileSystem extends FileSystem { - @override - void rename(String oldPath, String newPath) => using((arena) { + WindowsFileSystem() { // 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. + // + // TODO(brianquinlan): Remove this after it is fixed in the Dart SDK. win32.GetLastError(); + } + + @override + void rename(String oldPath, String newPath) => using((arena) { if (win32.MoveFileEx( oldPath.toNativeUtf16(allocator: arena), newPath.toNativeUtf16(allocator: arena), @@ -85,6 +218,126 @@ base class WindowsFileSystem extends FileSystem { } }); + /// 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) { + if ((isReadOnly ?? + isHidden ?? + isSystem ?? + needsArchive ?? + isTemporary ?? + isContentIndexed ?? + isOffline) == + null) { + return; + } + final fileInfo = arena(); + final nativePath = path.toNativeUtf16(allocator: arena); + int attributes; + if (original == null) { + if (win32.GetFileAttributesEx( + nativePath, + win32.GetFileExInfoStandard, + fileInfo, + ) == + win32.FALSE) { + final errorCode = win32.GetLastError(); + throw _getError(errorCode, 'set metadata failed', 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, 'set metadata failed', path); + } + }); + + @override + WindowsMetadata metadata(String path) => using((arena) { + final fileInfo = arena(); + if (win32.GetFileAttributesEx( + path.toNativeUtf16(allocator: arena), + win32.GetFileExInfoStandard, + fileInfo, + ) == + win32.FALSE) { + final errorCode = win32.GetLastError(); + throw _getError(errorCode, 'metadata failed', path); + } + final info = fileInfo.ref; + final attributes = info.dwFileAttributes; + return WindowsMetadata.fromFileAttributes( + attributes: attributes, + 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) { // Calling `GetLastError` for the first time causes the `GetLastError` diff --git a/pkgs/io_file/pubspec.yaml b/pkgs/io_file/pubspec.yaml index 2fac641a..f3f9bdc1 100644 --- a/pkgs/io_file/pubspec.yaml +++ b/pkgs/io_file/pubspec.yaml @@ -11,13 +11,18 @@ dependencies: ffi: ^2.1.4 stdlibc: git: - # Change this to a released version. + # TODO(brianquinlan): Change this to a released version. url: https://github.com/canonical/stdlibc.dart.git - win32: ^5.11.0 + win32: + git: + # TODO(brianquinlan): Change this to a released version. + url: https://github.com/halildurmus/win32.git + path: packages/win32 dev_dependencies: args: ^2.7.0 benchmark_harness: ^2.3.1 dart_flutter_team_lints: ^3.4.0 + path: ^1.9.1 test: ^1.24.0 uuid: ^4.5.1 diff --git a/pkgs/io_file/test/file_system_test.dart b/pkgs/io_file/test/file_system_test.dart index cf8c9f63..da4d5f3f 100644 --- a/pkgs/io_file/test/file_system_test.dart +++ b/pkgs/io_file/test/file_system_test.dart @@ -2,13 +2,12 @@ // 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 'package:io_file/io_file.dart'; import 'package:test/test.dart'; void main() { group('FileSystem', () { - test('rename', () { - expect(() => FileSystem().rename('a', 'b'), throwsUnsupportedError); + test('TODO(brianquinlan)', () { + // Add tests. }); }); } diff --git a/pkgs/io_file/test/metadata_test.dart b/pkgs/io_file/test/metadata_test.dart new file mode 100644 index 00000000..9b406ca3 --- /dev/null +++ b/pkgs/io_file/test/metadata_test.dart @@ -0,0 +1,97 @@ +// 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('windows') +library; + +import 'dart:io'; + +import 'package:io_file/io_file.dart'; +import 'package:test/test.dart'; + +import 'test_utils.dart'; + +void main() { + group('metadata', () { + late String tmp; + + setUp(() => tmp = createTemp('metadata')); + + tearDown(() => deleteTemp(tmp)); + + //TODO(brianquinlan): test with a very long path. + + test('path does not exist', () { + expect( + () => fileSystem.metadata('$tmp/file1'), + throwsA( + isA() + .having((e) => e.message, 'message', 'metadata failed') + .having( + (e) => e.osError?.errorCode, + 'errorCode', + 2, // ENOENT, ERROR_FILE_NOT_FOUND + ), + ), + ); + }); + + group('isDirectory/isFile/isLink', () { + test('directory', () { + final data = fileSystem.metadata(tmp); + expect(data.isDirectory, isTrue); + expect(data.isFile, isFalse); + expect(data.isLink, isFalse); + }); + test('file', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + + final data = fileSystem.metadata(path); + expect(data.isDirectory, isFalse); + expect(data.isFile, isTrue); + expect(data.isLink, isFalse); + }); + test('link', () { + File('$tmp/file1').writeAsStringSync('Hello World'); + final path = '$tmp/link'; + Link(path).createSync('$tmp/file1'); + + final data = fileSystem.metadata(path); + expect(data.isDirectory, isFalse); + expect(data.isFile, isFalse); + expect(data.isLink, isTrue); + }); + }); + + group('size', () { + test('directory', () { + final data = fileSystem.metadata(tmp); + expect(data.size, 0); + }); + test('empty file', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync(''); + + final data = fileSystem.metadata(path); + expect(data.size, 0); + }); + test('non-empty file', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World!'); + + final data = fileSystem.metadata(path); + expect(data.size, 12); + }); + test('link', () { + File('$tmp/file1').writeAsStringSync('Hello World'); + final path = '$tmp/link'; + Link(path).createSync('$tmp/file1'); + + final data = fileSystem.metadata(path); + expect(data.size, 0); + }); + }); + }); +} diff --git a/pkgs/io_file/test/metadata_windows_test.dart b/pkgs/io_file/test/metadata_windows_test.dart new file mode 100644 index 00000000..7ba83920 --- /dev/null +++ b/pkgs/io_file/test/metadata_windows_test.dart @@ -0,0 +1,497 @@ +// 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('windows') +library; + +import 'dart:io'; + +import 'package:io_file/src/vm_windows_file_system.dart'; +import 'package:test/test.dart'; + +import 'test_utils.dart'; + +void main() { + final windowsFileSystem = WindowsFileSystem(); + + group('windows metadata', () { + late String tmp; + + setUp(() => tmp = createTemp('metadata')); + + tearDown(() => deleteTemp(tmp)); + + group('isReadOnly', () { + test('false', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + + final data = windowsFileSystem.metadata(path); + expect(data.isReadOnly, isFalse); + }); + test('true', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + windowsFileSystem.setMetadata(path, isReadOnly: true); + + final data = windowsFileSystem.metadata(path); + expect(data.isReadOnly, isTrue); + }); + }); + + group('isHidden', () { + test('false', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + + final data = windowsFileSystem.metadata(path); + expect(data.isHidden, isFalse); + }); + test('true', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + windowsFileSystem.setMetadata(path, isHidden: true); + + final data = windowsFileSystem.metadata(path); + expect(data.isHidden, isTrue); + }); + }); + + group('isSystem', () { + test('false', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + + final data = windowsFileSystem.metadata(path); + expect(data.isSystem, isFalse); + }); + test('true', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + windowsFileSystem.setMetadata(path, isSystem: true); + + final data = windowsFileSystem.metadata(path); + expect(data.isSystem, isTrue); + }); + }); + + group('needsArchive', () { + test('false', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + windowsFileSystem.setMetadata(path, needsArchive: false); + + final data = windowsFileSystem.metadata(path); + expect(data.needsArchive, isFalse); + }); + test('true', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + windowsFileSystem.setMetadata(path, needsArchive: true); + + final data = windowsFileSystem.metadata(path); + expect(data.needsArchive, isTrue); + }); + }); + + group('isTemporary', () { + test('false', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + + final data = windowsFileSystem.metadata(path); + expect(data.isTemporary, isFalse); + }); + test('true', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + windowsFileSystem.setMetadata(path, isTemporary: true); + + final data = windowsFileSystem.metadata(path); + expect(data.isTemporary, isTrue); + }); + }); + + group('isContentNotIndexed', () { + test('false', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + + final data = windowsFileSystem.metadata(path); + expect(data.isContentIndexed, isFalse); + }); + test('true', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + windowsFileSystem.setMetadata(path, isContentIndexed: true); + + final data = windowsFileSystem.metadata(path); + expect(data.isContentIndexed, isTrue); + }); + }); + + group('isOffline', () { + test('false', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + + final data = windowsFileSystem.metadata(path); + expect(data.isOffline, isFalse); + }); + test('true', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + windowsFileSystem.setMetadata(path, isOffline: true); + + final data = windowsFileSystem.metadata(path); + expect(data.isOffline, isTrue); + }); + }); + + group('creation', () { + test('new file', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + final maxCreationTime = DateTime.now().millisecondsSinceEpoch; + + final data = windowsFileSystem.metadata(path); + expect( + data.creation.millisecondsSinceEpoch, + // Creation time within 1 second. + inInclusiveRange(maxCreationTime - 1000, maxCreationTime), + ); + }); + }); + + group('modificiation', () { + test('new file', () async { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + await Future.delayed(const Duration(seconds: 1)); + File(path).writeAsStringSync('How are you?'); + final maxModificationTime = DateTime.now().millisecondsSinceEpoch; + + final data = windowsFileSystem.metadata(path); + expect( + data.modification.millisecondsSinceEpoch, + inInclusiveRange( + data.creation.millisecondsSinceEpoch + 1000, + maxModificationTime, + ), + ); + }); + }); + + group('access', () { + test('new file', () async { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + File(path).readAsBytesSync(); + final maxAccessTime = DateTime.now().millisecondsSinceEpoch; + + final data = windowsFileSystem.metadata(path); + expect( + data.access.millisecondsSinceEpoch, + inInclusiveRange(data.creation.millisecondsSinceEpoch, maxAccessTime), + ); + }); + }); + }); + + group('set metadata', () { + late String tmp; + + setUp(() => tmp = createTemp('metadata')); + + tearDown(() => deleteTemp(tmp)); + + for (var includeOriginalMetadata in [true, false]) { + group('(use original metadata: $includeOriginalMetadata)', () { + group('start with all file attributes set', () { + late String path; + late WindowsMetadata initialMetadata; + + setUp(() { + path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + windowsFileSystem.setMetadata( + path, + isReadOnly: true, + isHidden: true, + isSystem: true, + needsArchive: true, + isTemporary: true, + isContentIndexed: true, + isOffline: true, + ); + initialMetadata = windowsFileSystem.metadata(path); + }); + + test('set none', () { + windowsFileSystem.setMetadata( + path, + original: includeOriginalMetadata ? initialMetadata : null, + ); + expect(windowsFileSystem.metadata(path), initialMetadata); + }); + test('unset isReadOnly', () { + windowsFileSystem.setMetadata( + path, + isReadOnly: false, + original: includeOriginalMetadata ? initialMetadata : null, + ); + + final data = windowsFileSystem.metadata(path); + expect(data.isReadOnly, isFalse); + expect(data.isHidden, isTrue); + expect(data.isSystem, isTrue); + expect(data.needsArchive, isTrue); + expect(data.isTemporary, isTrue); + expect(data.isContentIndexed, isTrue); + expect(data.isOffline, isTrue); + }); + + test('unset isHidden', () { + windowsFileSystem.setMetadata( + path, + isHidden: false, + original: includeOriginalMetadata ? initialMetadata : null, + ); + + final data = windowsFileSystem.metadata(path); + expect(data.isReadOnly, isTrue); + expect(data.isHidden, isFalse); + expect(data.isSystem, isTrue); + expect(data.needsArchive, isTrue); + expect(data.isTemporary, isTrue); + expect(data.isContentIndexed, isTrue); + expect(data.isOffline, isTrue); + }); + test('unset isSystem', () { + windowsFileSystem.setMetadata( + path, + isSystem: false, + original: includeOriginalMetadata ? initialMetadata : null, + ); + + final data = windowsFileSystem.metadata(path); + expect(data.isReadOnly, isTrue); + expect(data.isHidden, isTrue); + expect(data.isSystem, isFalse); + expect(data.needsArchive, isTrue); + expect(data.isTemporary, isTrue); + expect(data.isContentIndexed, isTrue); + expect(data.isOffline, isTrue); + }); + test('unset needsArchive', () { + windowsFileSystem.setMetadata( + path, + needsArchive: false, + original: includeOriginalMetadata ? initialMetadata : null, + ); + + final data = windowsFileSystem.metadata(path); + expect(data.isReadOnly, isTrue); + expect(data.isHidden, isTrue); + expect(data.isSystem, isTrue); + expect(data.needsArchive, isFalse); + expect(data.isTemporary, isTrue); + expect(data.isContentIndexed, isTrue); + expect(data.isOffline, isTrue); + }); + test('unset isTemporary', () { + windowsFileSystem.setMetadata( + path, + isTemporary: false, + original: includeOriginalMetadata ? initialMetadata : null, + ); + + final data = windowsFileSystem.metadata(path); + expect(data.isReadOnly, isTrue); + expect(data.isHidden, isTrue); + expect(data.isSystem, isTrue); + expect(data.needsArchive, isTrue); + expect(data.isTemporary, isFalse); + expect(data.isContentIndexed, isTrue); + expect(data.isOffline, isTrue); + }); + test('unset isContentNotIndexed', () { + windowsFileSystem.setMetadata( + path, + isContentIndexed: false, + original: includeOriginalMetadata ? initialMetadata : null, + ); + + final data = windowsFileSystem.metadata(path); + expect(data.isReadOnly, isTrue); + expect(data.isHidden, isTrue); + expect(data.isSystem, isTrue); + expect(data.needsArchive, isTrue); + expect(data.isTemporary, isTrue); + expect(data.isContentIndexed, isFalse); + expect(data.isOffline, isTrue); + }); + test('unset isOffline', () { + windowsFileSystem.setMetadata( + path, + isOffline: false, + original: includeOriginalMetadata ? initialMetadata : null, + ); + + final data = windowsFileSystem.metadata(path); + expect(data.isReadOnly, isTrue); + expect(data.isHidden, isTrue); + expect(data.isSystem, isTrue); + expect(data.needsArchive, isTrue); + expect(data.isTemporary, isTrue); + expect(data.isContentIndexed, isTrue); + expect(data.isOffline, isFalse); + }); + }); + + group('start with no file attributes set', () { + late String path; + late WindowsMetadata initialMetadata; + + setUp(() { + path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World'); + windowsFileSystem.setMetadata( + path, + isReadOnly: false, + isHidden: false, + isSystem: false, + needsArchive: false, + isTemporary: false, + isContentIndexed: false, + isOffline: false, + ); + initialMetadata = windowsFileSystem.metadata(path); + }); + + test('set none', () { + windowsFileSystem.setMetadata( + path, + original: includeOriginalMetadata ? initialMetadata : null, + ); + expect(windowsFileSystem.metadata(path), initialMetadata); + }); + test('set isReadOnly', () { + windowsFileSystem.setMetadata( + path, + isReadOnly: true, + original: includeOriginalMetadata ? initialMetadata : null, + ); + + final data = windowsFileSystem.metadata(path); + expect(data.isReadOnly, isTrue); + expect(data.isHidden, isFalse); + expect(data.isSystem, isFalse); + expect(data.needsArchive, isFalse); + expect(data.isTemporary, isFalse); + expect(data.isContentIndexed, isFalse); + expect(data.isOffline, isFalse); + }); + + test('set isHidden', () { + windowsFileSystem.setMetadata( + path, + isHidden: true, + original: includeOriginalMetadata ? initialMetadata : null, + ); + + final data = windowsFileSystem.metadata(path); + expect(data.isReadOnly, isFalse); + expect(data.isHidden, isTrue); + expect(data.isSystem, isFalse); + expect(data.needsArchive, isFalse); + expect(data.isTemporary, isFalse); + expect(data.isContentIndexed, isFalse); + expect(data.isOffline, isFalse); + }); + test('set isSystem', () { + windowsFileSystem.setMetadata( + path, + isSystem: true, + original: includeOriginalMetadata ? initialMetadata : null, + ); + + final data = windowsFileSystem.metadata(path); + expect(data.isReadOnly, isFalse); + expect(data.isHidden, isFalse); + expect(data.isSystem, isTrue); + expect(data.needsArchive, isFalse); + expect(data.isTemporary, isFalse); + expect(data.isContentIndexed, isFalse); + expect(data.isOffline, isFalse); + }); + test('set needsArchive', () { + windowsFileSystem.setMetadata( + path, + needsArchive: true, + original: includeOriginalMetadata ? initialMetadata : null, + ); + + final data = windowsFileSystem.metadata(path); + expect(data.isReadOnly, isFalse); + expect(data.isHidden, isFalse); + expect(data.isSystem, isFalse); + expect(data.needsArchive, isTrue); + expect(data.isTemporary, isFalse); + expect(data.isContentIndexed, isFalse); + expect(data.isOffline, isFalse); + }); + test('set isTemporary', () { + windowsFileSystem.setMetadata( + path, + isTemporary: true, + original: includeOriginalMetadata ? initialMetadata : null, + ); + + final data = windowsFileSystem.metadata(path); + expect(data.isReadOnly, isFalse); + expect(data.isHidden, isFalse); + expect(data.isSystem, isFalse); + expect(data.needsArchive, isFalse); + expect(data.isTemporary, isTrue); + expect(data.isContentIndexed, isFalse); + expect(data.isOffline, isFalse); + }); + test('set isContentNotIndexed', () { + windowsFileSystem.setMetadata( + path, + isContentIndexed: true, + original: includeOriginalMetadata ? initialMetadata : null, + ); + + final data = windowsFileSystem.metadata(path); + expect(data.isReadOnly, isFalse); + expect(data.isHidden, isFalse); + expect(data.isSystem, isFalse); + expect(data.needsArchive, isFalse); + expect(data.isTemporary, isFalse); + expect(data.isContentIndexed, isTrue); + expect(data.isOffline, isFalse); + }); + test('set isOffline', () { + windowsFileSystem.setMetadata( + path, + isOffline: true, + original: includeOriginalMetadata ? initialMetadata : null, + ); + + final data = windowsFileSystem.metadata(path); + expect(data.isReadOnly, isFalse); + expect(data.isHidden, isFalse); + expect(data.isSystem, isFalse); + expect(data.needsArchive, isFalse); + expect(data.isTemporary, isFalse); + expect(data.isContentIndexed, isFalse); + expect(data.isOffline, isTrue); + }); + }); + }); + } + }); +}