diff --git a/pkgs/io_file/lib/src/file_system.dart b/pkgs/io_file/lib/src/file_system.dart index d75fc16b..a5f2aebb 100644 --- a/pkgs/io_file/lib/src/file_system.dart +++ b/pkgs/io_file/lib/src/file_system.dart @@ -11,14 +11,97 @@ import 'package:meta/meta.dart' show sealed; // `dart:io` then change the doc strings to use reference syntax rather than // code syntax e.g. `PathExistsException` => [PathExistsException]. +/// The type of a file system object, such as a file or directory. +enum FileSystemType { + /// A special block file (also called a block device). + /// + /// Only exists on POSIX systems. + block, + + /// A file that represents a character device, such as a terminal or printer. + character, + + /// A container for other file system objects. + directory, + + /// A regular file. + file, + + /// A symbolic link. + link, + + /// A pipe, named pipe or FIFO. + pipe, + + /// A unix domain socket. + /// + /// Only exists on POSIX systems. + socket, + + /// The type of the file could not be determined. + unknown, +} + /// Information about a directory, link, etc. stored in the [FileSystem]. abstract interface class Metadata { - // TODO(brianquinlan): Document all public fields. + /// The type of the file system object. + FileSystemType get type; + /// Whether the file system object is a regular file. + /// + /// This will be `false` for some file system objects that can be read or + /// written to, such as sockets, pipse, and character devices. The most + /// reliable way to determine if a file system object can be read or written + /// to is to attempt to open it. + /// + /// At most one of [isDirectory], [isFile], or [isLink] will be `true`. bool get isFile; + + /// Whether the file system object is a directory. + /// + /// At most one of [isDirectory], [isFile], or [isLink] will be `true`. bool get isDirectory; + + /// Whether the file system object is symbolic link. + /// + /// At most one of [isDirectory], [isFile], or [isLink] will be `true`. bool get isLink; + + /// Whether the file system object is visible to the user. + /// + /// This will be `null` if the operating system does not support file system + /// visibility. It will always be `null` on Android and Linux. + bool? get isHidden; + + /// The size of the file system object in bytes. + /// + /// The `size` presented for file system objects other than regular files is + /// platform-specific. int get size; + + /// The time that the file system object was last accessed. + /// + /// Access time is updated when the object is read or modified. + /// + /// The resolution of the access time varies by platform and file system. + /// For example, FAT has an access time resolution of one day and NTFS may + /// delay updating the access time for up to one hour after the last access. + DateTime get access; + + /// The time that the file system object was created. + /// + /// This will always be `null` on platforms that do not track file creation + /// time. It will always be `null` on Android and Linux. + /// + /// The resolution of the creation time varies by platform and file system. + /// For example, FAT has a creation time resolution of 10 millseconds. + DateTime? get creation; + + /// The time that the file system object was last modified. + /// + /// The resolution of the modification time varies by platform and file + /// system. For example, FAT has a modification time resolution of 2 seconds. + DateTime get modification; } /// The modes in which a File can be written. @@ -88,10 +171,19 @@ abstract class FileSystem { /// ``` String createTemporaryDirectory({String? parent, String? prefix}); + /// TODO(brianquinlan): Add an `exists` method that can determine if a file + /// exists without mutating it on Windows (maybe using `FindFirstFile`?) + /// Metadata for the file system object at [path]. /// /// If `path` represents a symbolic link then metadata for the link is /// returned. + /// + /// On Windows, asking for the metadata for a named pipe may cause the server + /// to close it. + /// + /// The most reliable way to determine if a file system object can be read or + /// written to is to attempt to open it. Metadata metadata(String path); /// Deletes the directory at the given path. 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 36e93b2e..2caf7b5e 100644 --- a/pkgs/io_file/lib/src/vm_posix_file_system.dart +++ b/pkgs/io_file/lib/src/vm_posix_file_system.dart @@ -21,6 +21,8 @@ const _defaultMode = 438; // => 0666 => rw-rw-rw- /// The default `mode` to use when creating a directory. const _defaultDirectoryMode = 511; // => 0777 => rwxrwxrwx +const _nanosecondsPerSecond = 1000000000; + 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 @@ -50,6 +52,147 @@ int _tempFailureRetry(int Function() f) { return result; } +/// Information about a directory, link, etc. stored in the [PosixFileSystem]. +final class PosixMetadata implements Metadata { + /// The `st_mode` field of the POSIX stat struct. + /// + /// See [stat.h](https://pubs.opengroup.org/onlinepubs/009696799/basedefs/sys/stat.h.html) + /// for information on how to interpret this field. + final int mode; + final int _flags; + + @override + final int size; + + /// The time that the file system object was last accessed in nanoseconds + /// since the epoch. + /// + /// Access time is updated when the object is read or modified. + /// + /// The resolution of the access time varies by platform and file system. + final int accessedTimeNanos; + + /// The time that the file system object was created in nanoseconds since the + /// epoch. + /// + /// This will always be `null` on Android and Linux. + /// + /// The resolution of the creation time varies by platform and file system. + final int? creationTimeNanos; + + /// The time that the file system object was last modified in nanoseconds + /// since the epoch. + /// + /// The resolution of the modification time varies by platform and file + /// system. + final int modificationTimeNanos; + + int get _fmt => mode & libc.S_IFMT; + + @override + FileSystemType get type { + if (_fmt == libc.S_IFBLK) { + return FileSystemType.block; + } + if (_fmt == libc.S_IFCHR) { + return FileSystemType.character; + } + if (_fmt == libc.S_IFDIR) { + return FileSystemType.directory; + } + if (_fmt == libc.S_IFREG) { + return FileSystemType.file; + } + if (_fmt == libc.S_IFLNK) { + return FileSystemType.link; + } + if (_fmt == libc.S_IFIFO) { + return FileSystemType.pipe; + } + if (_fmt == libc.S_IFSOCK) { + return FileSystemType.socket; + } + return FileSystemType.unknown; + } + + @override + bool get isDirectory => type == FileSystemType.directory; + + @override + bool get isFile => type == FileSystemType.file; + + @override + bool get isLink => type == FileSystemType.link; + + @override + DateTime get access => + DateTime.fromMicrosecondsSinceEpoch(accessedTimeNanos ~/ 1000); + + @override + DateTime? get creation => + creationTimeNanos == null + ? null + : DateTime.fromMicrosecondsSinceEpoch(creationTimeNanos! ~/ 1000); + + @override + DateTime get modification => + DateTime.fromMicrosecondsSinceEpoch(modificationTimeNanos ~/ 1000); + + @override + bool? get isHidden { + if (io.Platform.isIOS || io.Platform.isMacOS) { + return _flags & libc.UF_HIDDEN != 0; + } + return null; + } + + PosixMetadata._( + this.mode, + this._flags, + this.size, + this.accessedTimeNanos, + this.creationTimeNanos, + this.modificationTimeNanos, + ); + + /// Construct [PosixMetadata] from data returned by the `stat` system call. + factory PosixMetadata.fromFileAttributes({ + required int mode, + int flags = 0, + int size = 0, + int accessedTimeNanos = 0, + int? creationTimeNanos, + int modificationTimeNanos = 0, + }) => PosixMetadata._( + mode, + flags, + size, + accessedTimeNanos, + creationTimeNanos, + modificationTimeNanos, + ); + + @override + bool operator ==(Object other) => + other is PosixMetadata && + mode == other.mode && + _flags == other._flags && + size == other.size && + accessedTimeNanos == other.accessedTimeNanos && + creationTimeNanos == other.creationTimeNanos && + modificationTimeNanos == other.modificationTimeNanos; + + @override + int get hashCode => Object.hash( + mode, + _flags, + size, + accessedTimeNanos, + creationTimeNanos, + modificationTimeNanos, + ); +} + /// The POSIX `read` function. /// /// See https://pubs.opengroup.org/onlinepubs/9699919799/functions/read.html @@ -112,9 +255,29 @@ final class PosixFileSystem extends FileSystem { }); @override - Metadata metadata(String path) { - throw UnimplementedError(); - } + PosixMetadata metadata(String path) => ffi.using((arena) { + final stat = arena(); + + if (libc.lstat(path.toNativeUtf8(allocator: arena).cast(), stat) == -1) { + final errno = libc.errno; + throw _getError(errno, 'stat failed', path); + } + + return PosixMetadata.fromFileAttributes( + mode: stat.ref.st_mode, + flags: stat.ref.st_flags, + size: stat.ref.st_size, + accessedTimeNanos: + stat.ref.st_atim.tv_sec * _nanosecondsPerSecond + + stat.ref.st_atim.tv_sec, + creationTimeNanos: + stat.ref.st_btime.tv_sec * _nanosecondsPerSecond + + stat.ref.st_btime.tv_sec, + modificationTimeNanos: + stat.ref.st_mtim.tv_sec * _nanosecondsPerSecond + + stat.ref.st_mtim.tv_sec, + ); + }); @override void removeDirectory(String path) => ffi.using((arena) { 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 d362016e..96d204fb 100644 --- a/pkgs/io_file/lib/src/vm_windows_file_system.dart +++ b/pkgs/io_file/lib/src/vm_windows_file_system.dart @@ -94,12 +94,37 @@ final class WindowsMetadata implements Metadata { /// Will never have the `FILE_ATTRIBUTE_NORMAL` bit set. int _attributes; + int _fileType; @override - bool get isDirectory => _attributes & win32.FILE_ATTRIBUTE_DIRECTORY != 0; + 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 => !isDirectory && !isLink; + bool get isFile => type == FileSystemType.file; @override bool get isLink => _attributes & win32.FILE_ATTRIBUTE_REPARSE_POINT != 0; @@ -108,6 +133,7 @@ final class WindowsMetadata implements Metadata { 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; @@ -123,12 +149,18 @@ final class WindowsMetadata implements Metadata { final int lastAccessTime100Nanos; final int lastWriteTime100Nanos; - DateTime get creation => _fileTimeToDateTime(creationTime100Nanos); + @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, @@ -141,12 +173,14 @@ final class WindowsMetadata implements Metadata { /// [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, @@ -155,8 +189,7 @@ final class WindowsMetadata implements Metadata { /// TODO(bquinlan): Document this constructor. factory WindowsMetadata.fromLogicalProperties({ - bool isDirectory = false, - bool isLink = false, + FileSystemType type = FileSystemType.unknown, int size = 0, @@ -172,8 +205,8 @@ final class WindowsMetadata implements Metadata { int lastAccessTime100Nanos = 0, int lastWriteTime100Nanos = 0, }) => WindowsMetadata._( - (isDirectory ? win32.FILE_ATTRIBUTE_DIRECTORY : 0) | - (isLink ? win32.FILE_ATTRIBUTE_REPARSE_POINT : 0) | + (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) | @@ -181,6 +214,12 @@ final class WindowsMetadata implements Metadata { (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, @@ -191,6 +230,7 @@ final class WindowsMetadata implements Metadata { bool operator ==(Object other) => other is WindowsMetadata && _attributes == other._attributes && + _fileType == other._fileType && size == other.size && creationTime100Nanos == other.creationTime100Nanos && lastAccessTime100Nanos == other.lastAccessTime100Nanos && @@ -199,6 +239,7 @@ final class WindowsMetadata implements Metadata { @override int get hashCode => Object.hash( _attributes, + _fileType, size, isContentIndexed, creationTime100Nanos, @@ -423,9 +464,10 @@ final class WindowsFileSystem extends FileSystem { WindowsMetadata metadata(String path) => using((arena) { _primeGetLastError(); + final pathUtf16 = path.toNativeUtf16(allocator: arena); final fileInfo = arena(); if (win32.GetFileAttributesEx( - path.toNativeUtf16(allocator: arena), + pathUtf16, win32.GetFileExInfoStandard, fileInfo, ) == @@ -435,8 +477,31 @@ final class WindowsFileSystem extends FileSystem { } 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 | @@ -562,8 +627,8 @@ final class WindowsFileSystem extends FileSystem { bool same(String path1, String path2) => using((arena) { _primeGetLastError(); - final info1 = _byHandleFileInformation(path1, arena); - final info2 = _byHandleFileInformation(path2, arena); + final info1 = _byHandleFileInformationFromPath(path1, arena); + final info2 = _byHandleFileInformationFromPath(path2, arena); return info1.dwVolumeSerialNumber == info2.dwVolumeSerialNumber && info1.nFileIndexHigh == info2.nFileIndexHigh && @@ -571,12 +636,23 @@ final class WindowsFileSystem extends FileSystem { }); // NOTE: the return value is allocated in the given arena! - static win32.BY_HANDLE_FILE_INFORMATION _byHandleFileInformation( + static win32.BY_HANDLE_FILE_INFORMATION _byHandleFileInformationFromPath( String path, ffi.Arena arena, + ) => _byHandleFileInformationFromPath16( + path, + path.toNativeUtf16(allocator: arena), + arena, + ); + + // NOTE: the return value is allocated in the given arena! + static win32.BY_HANDLE_FILE_INFORMATION _byHandleFileInformationFromPath16( + String path, + Pointer path16, + ffi.Arena arena, ) { final h = win32.CreateFile( - path.toNativeUtf16(allocator: arena), + path16, 0, win32.FILE_SHARE_READ | win32.FILE_SHARE_WRITE | win32.FILE_SHARE_DELETE, nullptr, diff --git a/pkgs/io_file/test/fifo_posix.dart b/pkgs/io_file/test/fifo_posix.dart index a5d53c73..b6ecb9ac 100644 --- a/pkgs/io_file/test/fifo_posix.dart +++ b/pkgs/io_file/test/fifo_posix.dart @@ -22,8 +22,10 @@ class FifoPosix implements Fifo { static Future create(String suggestedPath) async { final p = ReceivePort(); - stdlibc.mkfifo(suggestedPath, 438); // 0436 => permissions: -rw-rw-rw- - + // 0436 => permissions: -rw-rw-rw- + if (stdlibc.mkfifo(suggestedPath, 438) == -1) { + throw AssertionError('mkfifo failed: ${stdlibc.errno}'); + } await Isolate.spawn((port) { final receivePort = ReceivePort(); port.send(receivePort.sendPort); @@ -32,7 +34,6 @@ class FifoPosix implements Fifo { suggestedPath, flags: stdlibc.O_WRONLY | stdlibc.O_CLOEXEC, ); - if (fd == -1) { throw AssertionError('could not open fifo: ${stdlibc.errno}'); } diff --git a/pkgs/io_file/test/fifo_windows.dart b/pkgs/io_file/test/fifo_windows.dart index 26ccd6d5..bfa4568c 100644 --- a/pkgs/io_file/test/fifo_windows.dart +++ b/pkgs/io_file/test/fifo_windows.dart @@ -52,7 +52,6 @@ class FifoWindows implements Fifo { final receivePort = ReceivePort(); port.send(receivePort.sendPort); - if (win32.ConnectNamedPipe(f, nullptr) == win32.FALSE) { final error = win32.GetLastError(); if (error != diff --git a/pkgs/io_file/test/metadata_apple_test.dart b/pkgs/io_file/test/metadata_apple_test.dart new file mode 100644 index 00000000..d4286d93 --- /dev/null +++ b/pkgs/io_file/test/metadata_apple_test.dart @@ -0,0 +1,51 @@ +// 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('ios || mac-os') +library; + +import 'dart:ffi'; +import 'dart:io'; + +import 'package:ffi/ffi.dart'; +import 'package:io_file/src/vm_posix_file_system.dart'; +import 'package:stdlibc/stdlibc.dart' as stdlibc; +import 'package:test/test.dart'; + +import 'test_utils.dart'; + +@Native, Uint32)>(isLeaf: false) +external int chflags(Pointer buf, int count); + +void main() { + final posixFileSystem = PosixFileSystem(); + + group('apple metadata', () { + late String tmp; + + setUp(() => tmp = createTemp('metadata')); + + tearDown(() => deleteTemp(tmp)); + + group('isHidden', () { + test('false', () { + final path = '$tmp/file'; + File(path).writeAsStringSync('Hello World'); + + final data = posixFileSystem.metadata(path); + expect(data.isHidden, isFalse); + }); + test('true', () { + final path = '$tmp/file'; + File(path).writeAsStringSync('Hello World'); + using((arena) { + chflags(path.toNativeUtf8(), stdlibc.UF_HIDDEN); + }); + + final data = posixFileSystem.metadata(path); + expect(data.isHidden, isTrue); + }); + }); + }); +} diff --git a/pkgs/io_file/test/metadata_test.dart b/pkgs/io_file/test/metadata_test.dart index 9b406ca3..1fa769ac 100644 --- a/pkgs/io_file/test/metadata_test.dart +++ b/pkgs/io_file/test/metadata_test.dart @@ -2,14 +2,17 @@ // 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') +@TestOn('vm') library; import 'dart:io'; import 'package:io_file/io_file.dart'; import 'package:test/test.dart'; +import 'package:win32/win32.dart' as win32; +import 'errors.dart' as errors; +import 'fifo.dart'; import 'test_utils.dart'; void main() { @@ -26,24 +29,40 @@ void main() { 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 - ), + isA().having( + (e) => e.osError?.errorCode, + 'errorCode', + Platform.isWindows ? win32.ERROR_FILE_NOT_FOUND : errors.enoent, + ), ), ); }); - group('isDirectory/isFile/isLink', () { + group('file types', () { test('directory', () { final data = fileSystem.metadata(tmp); expect(data.isDirectory, isTrue); expect(data.isFile, isFalse); expect(data.isLink, isFalse); + expect(data.type, FileSystemType.directory); }); + test( + 'tty', + () { + final data = fileSystem.metadata('/dev/tty'); + expect(data.isDirectory, isFalse); + expect(data.isFile, isFalse); + expect(data.isLink, isFalse); + expect(data.type, FileSystemType.character); + }, + skip: + !(Platform.isAndroid | + Platform.isIOS | + Platform.isLinux | + Platform.isIOS) + ? 'no /dev/tty' + : false, + ); test('file', () { final path = '$tmp/file1'; File(path).writeAsStringSync('Hello World'); @@ -52,8 +71,27 @@ void main() { expect(data.isDirectory, isFalse); expect(data.isFile, isTrue); expect(data.isLink, isFalse); + expect(data.type, FileSystemType.file); }); - test('link', () { + test('fifo', () async { + final fifo = (await Fifo.create('$tmp/file'))..close(); + + final data = fileSystem.metadata(fifo.path); + expect(data.isDirectory, isFalse); + expect(data.isFile, isFalse); + expect(data.isLink, isFalse); + expect( + data.type, + Platform.isWindows ? FileSystemType.unknown : FileSystemType.pipe, + ); + + try { + // On Windows, opening the pipe consumes it. See: + // https://github.com/dotnet/runtime/issues/69604 + fileSystem.readAsBytes(fifo.path); + } catch (_) {} + }); + test('file link', () { File('$tmp/file1').writeAsStringSync('Hello World'); final path = '$tmp/link'; Link(path).createSync('$tmp/file1'); @@ -62,14 +100,39 @@ void main() { expect(data.isDirectory, isFalse); expect(data.isFile, isFalse); expect(data.isLink, isTrue); + expect(data.type, FileSystemType.link); + }); + test('directory link', () { + Directory('$tmp/dir').createSync(); + final path = '$tmp/link'; + Link(path).createSync('$tmp/dir'); + + final data = fileSystem.metadata(path); + expect(data.isDirectory, isFalse); + expect(data.isFile, isFalse); + expect(data.isLink, isTrue); + expect(data.type, FileSystemType.link); }); }); + test( + 'isHidden', + () { + // Tested on iOS/macOS at: metadata_apple_test.dart + // Tested on Windows at: metadata_windows_test.dart + + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World!'); + + final data = fileSystem.metadata(path); + expect(data.isHidden, isNull); + }, + skip: + (Platform.isIOS || Platform.isMacOS || Platform.isWindows) + ? 'does not support hidden file metadata' + : false, + ); group('size', () { - test('directory', () { - final data = fileSystem.metadata(tmp); - expect(data.size, 0); - }); test('empty file', () { final path = '$tmp/file1'; File(path).writeAsStringSync(''); @@ -84,13 +147,52 @@ void main() { 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'); + }); + + group( + 'creation', + () { + test('newly created', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync(''); + + final data = fileSystem.metadata(path); + expect( + data.creation!.millisecondsSinceEpoch, + closeTo(DateTime.now().millisecondsSinceEpoch, 5000), + ); + }); + }, + skip: + !(Platform.isIOS || Platform.isMacOS || Platform.isWindows) + ? 'creation not supported' + : false, + ); + + group('modification', () { + test('newly created', () { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World!'); final data = fileSystem.metadata(path); - expect(data.size, 0); + expect( + data.modification.millisecondsSinceEpoch, + closeTo(DateTime.now().millisecondsSinceEpoch, 5000), + ); + }); + test('modified after creation', () async { + final path = '$tmp/file1'; + File(path).writeAsStringSync('Hello World!'); + final data1 = fileSystem.metadata(path); + + await Future.delayed(const Duration(milliseconds: 1000)); + File(path).writeAsStringSync('Hello World!'); + final data2 = fileSystem.metadata(path); + + expect( + data2.modification.millisecondsSinceEpoch, + greaterThan(data1.modification.millisecondsSinceEpoch), + ); }); }); });