Skip to content

Commit 1d0a0eb

Browse files
authored
Add support for same on POSIX (#211)
1 parent 371d4fb commit 1d0a0eb

File tree

4 files changed

+261
-1
lines changed

4 files changed

+261
-1
lines changed

pkgs/io_file/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ See
2323
| enum dir contents | | | | | | | |
2424
| exists | | | | | | | |
2525
| get metadata (stat) | | | | | | | |
26-
| identity (same file) | | | | | | | |
26+
| identity (same file) | | | | | | | |
2727
| open | | | | | | | |
2828
| read file (bytes) | ||||| | |
2929
| read file (lines) | | | | | | | |

pkgs/io_file/lib/src/file_system.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ class WriteMode {
3434

3535
/// An abstract representation of a file system.
3636
base class FileSystem {
37+
/// Checks whether two paths refer to the same object in the file system.
38+
///
39+
/// Throws `PathNotFoundException` if either path doesn't exist.
40+
///
41+
/// Links are resolved before determining if the paths refer to the same
42+
/// object. Throws `PathNotFoundException` if either path requires resolving
43+
/// a broken link.
44+
bool same(String path1, String path2) {
45+
throw UnsupportedError('same');
46+
}
47+
3748
/// Create a directory at the given path.
3849
///
3950
/// If the directory already exists, then `PathExistsException` is thrown.

pkgs/io_file/lib/src/vm_posix_file_system.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,23 @@ external int write(int fd, Pointer<Uint8> buf, int count);
6060
/// A [FileSystem] implementation for POSIX systems (e.g. Android, iOS, Linux,
6161
/// macOS).
6262
base class PosixFileSystem extends FileSystem {
63+
@override
64+
bool same(String path1, String path2) {
65+
final stat1 = stdlibc.stat(path1);
66+
if (stat1 == null) {
67+
final errno = stdlibc.errno;
68+
throw _getError(errno, 'stat failed', path1);
69+
}
70+
71+
final stat2 = stdlibc.stat(path2);
72+
if (stat2 == null) {
73+
final errno = stdlibc.errno;
74+
throw _getError(errno, 'stat failed', path2);
75+
}
76+
77+
return (stat1.st_ino == stat2.st_ino) && (stat1.st_dev == stat2.st_dev);
78+
}
79+
6380
@override
6481
void createDirectory(String path) {
6582
if (stdlibc.mkdir(path, _defaultDirectoryMode) == -1) {

pkgs/io_file/test/same_test.dart

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
@TestOn('posix')
6+
library;
7+
8+
import 'dart:io';
9+
10+
import 'package:io_file/io_file.dart';
11+
import 'package:stdlibc/stdlibc.dart' as stdlibc;
12+
import 'package:test/test.dart';
13+
14+
import 'test_utils.dart';
15+
16+
void main() {
17+
group('same', () {
18+
late String tmp;
19+
late Directory cwd;
20+
21+
setUp(() {
22+
tmp = createTemp('same');
23+
cwd = Directory.current;
24+
});
25+
26+
tearDown(() {
27+
Directory.current = cwd;
28+
deleteTemp(tmp);
29+
});
30+
31+
//TODO(brianquinlan): test with a very long path.
32+
33+
test('path1 does not exist', () {
34+
final path1 = '$tmp/file1';
35+
final path2 = '$tmp/file2';
36+
File(path2).writeAsStringSync('Hello World');
37+
38+
expect(
39+
() => fileSystem.same(path1, path2),
40+
throwsA(
41+
isA<PathNotFoundException>()
42+
.having((e) => e.message, 'message', 'stat failed')
43+
.having((e) => e.path, 'path', path1)
44+
.having((e) => e.osError?.errorCode, 'errorCode', stdlibc.ENOENT),
45+
),
46+
);
47+
});
48+
49+
test('path2 does not exist', () {
50+
final path1 = '$tmp/file1';
51+
final path2 = '$tmp/file2';
52+
File(path1).writeAsStringSync('Hello World');
53+
54+
expect(
55+
() => fileSystem.same(path1, path2),
56+
throwsA(
57+
isA<PathNotFoundException>()
58+
.having((e) => e.message, 'message', 'stat failed')
59+
.having((e) => e.path, 'path', path2)
60+
.having((e) => e.osError?.errorCode, 'errorCode', stdlibc.ENOENT),
61+
),
62+
);
63+
});
64+
65+
test('path1 and path2 same, do not exist', () {
66+
final path1 = '$tmp/file1';
67+
final path2 = '$tmp/file1';
68+
69+
expect(
70+
() => fileSystem.same(path1, path2),
71+
throwsA(
72+
isA<PathNotFoundException>()
73+
.having((e) => e.message, 'message', 'stat failed')
74+
.having((e) => e.path, 'path', path1)
75+
.having((e) => e.osError?.errorCode, 'errorCode', stdlibc.ENOENT),
76+
),
77+
);
78+
});
79+
80+
test('path1 is a broken symlink', () {
81+
final path1 = '$tmp/file1';
82+
final path2 = '$tmp/file2';
83+
Link(path1).createSync('$tmp/file3');
84+
File(path2).writeAsStringSync('Hello World');
85+
86+
expect(
87+
() => fileSystem.same(path1, path2),
88+
throwsA(
89+
isA<PathNotFoundException>()
90+
.having((e) => e.message, 'message', 'stat failed')
91+
.having((e) => e.path, 'path', path1)
92+
.having((e) => e.osError?.errorCode, 'errorCode', stdlibc.ENOENT),
93+
),
94+
);
95+
});
96+
97+
test('path2 is a broken symlink', () {
98+
final path1 = '$tmp/file1';
99+
final path2 = '$tmp/file2';
100+
File(path1).writeAsStringSync('Hello World');
101+
Link(path2).createSync('$tmp/file3');
102+
103+
expect(
104+
() => fileSystem.same(path1, path2),
105+
throwsA(
106+
isA<PathNotFoundException>()
107+
.having((e) => e.message, 'message', 'stat failed')
108+
.having((e) => e.path, 'path', path2)
109+
.having((e) => e.osError?.errorCode, 'errorCode', stdlibc.ENOENT),
110+
),
111+
);
112+
});
113+
114+
test('path1 and path2 same, broken symlinks', () {
115+
final path1 = '$tmp/file1';
116+
final path2 = '$tmp/file1';
117+
Link(path1).createSync('$tmp/file3');
118+
119+
expect(
120+
() => fileSystem.same(path1, path2),
121+
throwsA(
122+
isA<PathNotFoundException>()
123+
.having((e) => e.message, 'message', 'stat failed')
124+
.having((e) => e.path, 'path', path1)
125+
.having((e) => e.osError?.errorCode, 'errorCode', stdlibc.ENOENT),
126+
),
127+
);
128+
});
129+
130+
test('different files, same content', () {
131+
final path1 = '$tmp/file1';
132+
final path2 = '$tmp/file2';
133+
File(path1).writeAsStringSync('Hello World');
134+
File(path2).writeAsStringSync('Hello World');
135+
136+
expect(fileSystem.same(path1, path2), isFalse);
137+
});
138+
139+
test('same file, absolute and relative paths', () {
140+
Directory.current = tmp;
141+
final path1 = '$tmp/file1';
142+
const path2 = 'file1';
143+
File(path1).writeAsStringSync('Hello World');
144+
145+
expect(fileSystem.same(path1, path2), isTrue);
146+
});
147+
148+
test('file path1, symlink path2', () {
149+
final path1 = '$tmp/file1';
150+
final path2 = '$tmp/file2';
151+
File(path1).writeAsStringSync('Hello World');
152+
Link(path2).createSync(path1);
153+
154+
expect(fileSystem.same(path1, path2), isTrue);
155+
});
156+
157+
test('file symlink path1, symlink path2', () {
158+
final path1 = '$tmp/file1';
159+
final path2 = '$tmp/file2';
160+
File('$tmp/file3').writeAsStringSync('Hello World');
161+
Link(path1).createSync('$tmp/file3');
162+
Link(path2).createSync('$tmp/file3');
163+
164+
expect(fileSystem.same(path1, path2), isTrue);
165+
});
166+
167+
test('files through intermediate symlinks', () {
168+
Directory('$tmp/subdir').createSync();
169+
Link('$tmp/link-to-subdir').createSync('$tmp/subdir');
170+
final path1 = '$tmp/subdir/file1';
171+
final path2 = '$tmp/link-to-subdir/file1';
172+
File(path1).writeAsStringSync('Hello World');
173+
174+
expect(fileSystem.same(path1, path2), isTrue);
175+
});
176+
177+
test('hard links to same file', () {
178+
final path1 = '$tmp/file1';
179+
final path2 = '$tmp/file2';
180+
File(path1).writeAsStringSync('Hello World');
181+
stdlibc.link(path1, path2);
182+
expect(fileSystem.same(path1, path2), isTrue);
183+
});
184+
185+
test('different directories, same content', () {
186+
final path1 = '$tmp/dir1';
187+
final path2 = '$tmp/dir2';
188+
Directory(path1).createSync();
189+
Directory(path2).createSync();
190+
191+
expect(fileSystem.same(path1, path2), isFalse);
192+
});
193+
194+
test('same directory, absolute and relative paths', () {
195+
Directory.current = tmp;
196+
final path1 = '$tmp/dir1';
197+
const path2 = 'dir1';
198+
Directory(path1).createSync();
199+
200+
expect(fileSystem.same(path1, path2), isTrue);
201+
});
202+
203+
test('directory path1, symlink path2', () {
204+
final path1 = '$tmp/dir1';
205+
final path2 = '$tmp/dir2';
206+
Directory(path1).createSync();
207+
Link(path2).createSync(path1);
208+
209+
expect(fileSystem.same(path1, path2), isTrue);
210+
});
211+
212+
test('directory symlink path1, symlink path2', () {
213+
final path1 = '$tmp/dir1';
214+
final path2 = '$tmp/dir2';
215+
Directory('$tmp/dir3').createSync();
216+
Link(path1).createSync('$tmp/dir3');
217+
Link(path2).createSync('$tmp/dir3');
218+
219+
expect(fileSystem.same(path1, path2), isTrue);
220+
});
221+
222+
test('directories through intermediate symlinks', () {
223+
Directory('$tmp/subdir').createSync();
224+
Link('$tmp/link-to-subdir').createSync('$tmp/subdir');
225+
final path1 = '$tmp/subdir/dir1';
226+
final path2 = '$tmp/link-to-subdir/dir1';
227+
Directory(path1).createSync();
228+
229+
expect(fileSystem.same(path1, path2), isTrue);
230+
});
231+
});
232+
}

0 commit comments

Comments
 (0)