-
Notifications
You must be signed in to change notification settings - Fork 5
Add support for same
on POSIX
#211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,6 +30,17 @@ class WriteMode { | |
|
||
/// An abstract representation of a file system. | ||
base class FileSystem { | ||
/// Checks whether two paths refer to the same object in the file system. | ||
/// | ||
/// Throws `PathNotFoundException` if either path doesn't exist. | ||
/// | ||
/// Links are resolved before determining if the paths refer to the same | ||
/// object. Throws `PathNotFoundException` if either path requires resolving | ||
/// a broken link. | ||
bool same(String path1, String path2) { | ||
throw UnsupportedError('same'); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe just not have this method, if it's not supported on all file systems. If you have to do If you plan to implement it on all file systems, make it abstract here, to force an implementation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It will be implemented on all file systems. |
||
/// 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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -57,6 +57,23 @@ external int write(int fd, Pointer<Uint8> buf, int count); | |
/// A [FileSystem] implementation for POSIX systems (e.g. Android, iOS, Linux, | ||
/// macOS). | ||
base class PosixFileSystem extends FileSystem { | ||
@override | ||
bool same(String path1, String path2) { | ||
final stat1 = stdlibc.stat(path1); | ||
if (stat1 == null) { | ||
final errno = stdlibc.errno; | ||
throw _getError(errno, 'stat failed', path1); | ||
} | ||
|
||
final stat2 = stdlibc.stat(path2); | ||
if (stat2 == null) { | ||
final errno = stdlibc.errno; | ||
throw _getError(errno, 'stat failed', path2); | ||
} | ||
|
||
return (stat1.st_ino == stat2.st_ino) && (stat1.st_dev == stat2.st_dev); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like it equates filled with the same inode. That means that hard links, non-symbolic links, to the same inode should be equal, even if they resolve to filled with different paths. Should be tested too:
Generally, test with targets that are directories too. Do symlinks have inodes? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yep.
Will do. I'll need to make some upstream changes to
I already do this.
Yes, but
Link('foo').create('bar');
FileSystemEntity.identical('foo', 'bar'); // false
same('foo', 'bar'); // true I was thinking that maybe |
||
} | ||
|
||
@override | ||
void rename(String oldPath, String newPath) { | ||
// See https://pubs.opengroup.org/onlinepubs/000095399/functions/rename.html | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
// 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('posix') | ||
library; | ||
|
||
import 'dart:io'; | ||
|
||
import 'package:io_file/io_file.dart'; | ||
import 'package:stdlibc/stdlibc.dart' as stdlibc; | ||
import 'package:test/test.dart'; | ||
|
||
import 'test_utils.dart'; | ||
|
||
void main() { | ||
group('same', () { | ||
late String tmp; | ||
late Directory cwd; | ||
|
||
setUp(() { | ||
tmp = createTemp('same'); | ||
cwd = Directory.current; | ||
}); | ||
|
||
tearDown(() { | ||
Directory.current = cwd; | ||
deleteTemp(tmp); | ||
}); | ||
|
||
//TODO(brianquinlan): test with a very long path. | ||
|
||
test('path1 does not exist', () { | ||
final path1 = '$tmp/file1'; | ||
final path2 = '$tmp/file2'; | ||
File(path2).writeAsStringSync('Hello World'); | ||
|
||
expect( | ||
() => fileSystem.same(path1, path2), | ||
throwsA( | ||
isA<PathNotFoundException>() | ||
.having((e) => e.message, 'message', 'stat failed') | ||
.having((e) => e.path, 'path', path1) | ||
.having((e) => e.osError?.errorCode, 'errorCode', stdlibc.ENOENT), | ||
), | ||
); | ||
}); | ||
|
||
test('path2 does not exist', () { | ||
final path1 = '$tmp/file1'; | ||
final path2 = '$tmp/file2'; | ||
File(path1).writeAsStringSync('Hello World'); | ||
|
||
expect( | ||
() => fileSystem.same(path1, path2), | ||
throwsA( | ||
isA<PathNotFoundException>() | ||
.having((e) => e.message, 'message', 'stat failed') | ||
.having((e) => e.path, 'path', path2) | ||
.having((e) => e.osError?.errorCode, 'errorCode', stdlibc.ENOENT), | ||
), | ||
); | ||
}); | ||
|
||
test('path1 and path2 same, do not exist', () { | ||
final path1 = '$tmp/file1'; | ||
final path2 = '$tmp/file1'; | ||
|
||
expect( | ||
() => fileSystem.same(path1, path2), | ||
throwsA( | ||
isA<PathNotFoundException>() | ||
.having((e) => e.message, 'message', 'stat failed') | ||
.having((e) => e.path, 'path', path1) | ||
.having((e) => e.osError?.errorCode, 'errorCode', stdlibc.ENOENT), | ||
), | ||
); | ||
}); | ||
|
||
test('path1 is a broken symlink', () { | ||
final path1 = '$tmp/file1'; | ||
final path2 = '$tmp/file2'; | ||
Link(path1).createSync('$tmp/file3'); | ||
File(path2).writeAsStringSync('Hello World'); | ||
|
||
expect( | ||
() => fileSystem.same(path1, path2), | ||
throwsA( | ||
isA<PathNotFoundException>() | ||
.having((e) => e.message, 'message', 'stat failed') | ||
.having((e) => e.path, 'path', path1) | ||
.having((e) => e.osError?.errorCode, 'errorCode', stdlibc.ENOENT), | ||
), | ||
); | ||
}); | ||
|
||
test('path2 is a broken symlink', () { | ||
final path1 = '$tmp/file1'; | ||
final path2 = '$tmp/file2'; | ||
File(path1).writeAsStringSync('Hello World'); | ||
Link(path2).createSync('$tmp/file3'); | ||
|
||
expect( | ||
() => fileSystem.same(path1, path2), | ||
throwsA( | ||
isA<PathNotFoundException>() | ||
.having((e) => e.message, 'message', 'stat failed') | ||
.having((e) => e.path, 'path', path2) | ||
.having((e) => e.osError?.errorCode, 'errorCode', stdlibc.ENOENT), | ||
), | ||
); | ||
}); | ||
|
||
test('path1 and path2 same, broken symlinks', () { | ||
final path1 = '$tmp/file1'; | ||
final path2 = '$tmp/file1'; | ||
Link(path1).createSync('$tmp/file3'); | ||
|
||
expect( | ||
() => fileSystem.same(path1, path2), | ||
throwsA( | ||
isA<PathNotFoundException>() | ||
.having((e) => e.message, 'message', 'stat failed') | ||
.having((e) => e.path, 'path', path1) | ||
.having((e) => e.osError?.errorCode, 'errorCode', stdlibc.ENOENT), | ||
), | ||
); | ||
}); | ||
|
||
test('different files, same content', () { | ||
final path1 = '$tmp/file1'; | ||
final path2 = '$tmp/file2'; | ||
File(path1).writeAsStringSync('Hello World'); | ||
File(path2).writeAsStringSync('Hello World'); | ||
|
||
expect(fileSystem.same(path1, path2), isFalse); | ||
}); | ||
|
||
test('same file, absolute and relative paths', () { | ||
Directory.current = tmp; | ||
final path1 = '$tmp/file1'; | ||
const path2 = 'file1'; | ||
File(path1).writeAsStringSync('Hello World'); | ||
|
||
expect(fileSystem.same(path1, path2), isTrue); | ||
}); | ||
|
||
test('file path1, symlink path2', () { | ||
final path1 = '$tmp/file1'; | ||
final path2 = '$tmp/file2'; | ||
File(path1).writeAsStringSync('Hello World'); | ||
Link(path2).createSync(path1); | ||
|
||
expect(fileSystem.same(path1, path2), isTrue); | ||
}); | ||
|
||
test('file symlink path1, symlink path2', () { | ||
final path1 = '$tmp/file1'; | ||
final path2 = '$tmp/file2'; | ||
File('$tmp/file3').writeAsStringSync('Hello World'); | ||
Link(path1).createSync('$tmp/file3'); | ||
Link(path2).createSync('$tmp/file3'); | ||
|
||
expect(fileSystem.same(path1, path2), isTrue); | ||
}); | ||
|
||
test('files through intermediate symlinks', () { | ||
Directory('$tmp/subdir').createSync(); | ||
Link('$tmp/link-to-subdir').createSync('$tmp/subdir'); | ||
final path1 = '$tmp/subdir/file1'; | ||
final path2 = '$tmp/link-to-subdir/file1'; | ||
File(path1).writeAsStringSync('Hello World'); | ||
|
||
expect(fileSystem.same(path1, path2), isTrue); | ||
}); | ||
|
||
test('different directories, same content', () { | ||
final path1 = '$tmp/dir1'; | ||
final path2 = '$tmp/dir2'; | ||
Directory(path1).createSync(); | ||
Directory(path2).createSync(); | ||
|
||
expect(fileSystem.same(path1, path2), isFalse); | ||
}); | ||
|
||
test('same directory, absolute and relative paths', () { | ||
Directory.current = tmp; | ||
final path1 = '$tmp/dir1'; | ||
const path2 = 'dir1'; | ||
Directory(path1).createSync(); | ||
|
||
expect(fileSystem.same(path1, path2), isTrue); | ||
}); | ||
|
||
test('directory path1, symlink path2', () { | ||
final path1 = '$tmp/dir1'; | ||
final path2 = '$tmp/dir2'; | ||
Directory(path1).createSync(); | ||
Link(path2).createSync(path1); | ||
|
||
expect(fileSystem.same(path1, path2), isTrue); | ||
}); | ||
|
||
test('directory symlink path1, symlink path2', () { | ||
final path1 = '$tmp/dir1'; | ||
final path2 = '$tmp/dir2'; | ||
Directory('$tmp/dir3').createSync(); | ||
Link(path1).createSync('$tmp/dir3'); | ||
Link(path2).createSync('$tmp/dir3'); | ||
|
||
expect(fileSystem.same(path1, path2), isTrue); | ||
}); | ||
|
||
test('directories through intermediate symlinks', () { | ||
Directory('$tmp/subdir').createSync(); | ||
Link('$tmp/link-to-subdir').createSync('$tmp/subdir'); | ||
final path1 = '$tmp/subdir/dir1'; | ||
final path2 = '$tmp/link-to-subdir/dir1'; | ||
Directory(path1).createSync(); | ||
|
||
expect(fileSystem.same(path1, path2), isTrue); | ||
}); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make this class abstract?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've been discussing this at #202
I'll leave it unchanged here and hopefully we can come to a conclusion in that PR ;-)