@react-native-gpui/fs 0.0.0-stage → 0.0.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,286 @@
1
+ //! `@react-native-gpui/fs`: a Node-shaped file API for the desktop app.
2
+ //!
3
+ //! The TypeScript spec in `../src/NativeFs.ts` is the contract; the generator
4
+ //! turns it into the `NativeFsSpec` trait implemented here. That trait is the
5
+ //! only thing the C++ bridge knows about, so the API surface is edited there and
6
+ //! regenerated, never in this file.
7
+ //!
8
+ //! Rust needs a trait's methods in a single `impl` block, so this file holds that
9
+ //! block and hands each method to a helper in the file that owns the concern:
10
+ //!
11
+ //! - `files` — reading, writing, copying, truncating, permissions and times
12
+ //! - `dirs` — listing, creating, removing and `mkdtemp`
13
+ //! - `handles` — open descriptors, positioned reads and writes
14
+ //! - `links` — symbolic links, `realpath` and `access`
15
+ //! - `paths` — the well-known folders, created on first read
16
+ //! - `dialogs` — the pickers, `reveal` and `openExternal`
17
+ //! - `watching` — the watcher behind `watch` and `unwatch`
18
+ //!
19
+ //! Keeping the bodies in those files is what makes the crate readable; keeping
20
+ //! the `impl` here is what makes it compile.
21
+
22
+ rn_gpui_module::include_specs!();
23
+
24
+ mod dialogs;
25
+ mod dirs;
26
+ mod download;
27
+ mod errors;
28
+ mod files;
29
+ mod handles;
30
+ mod links;
31
+ mod module;
32
+ mod paths;
33
+ mod upload;
34
+ mod watching;
35
+
36
+ pub use module::RNFsModule;
37
+
38
+ use rn_gpui_module::{ArrayBuffer, ModuleError};
39
+
40
+ impl NativeFsSpec for RNFsModule {
41
+ fn new(events: NativeFsEvents) -> Self {
42
+ Self::create(events)
43
+ }
44
+
45
+ async fn read_file(&self, path: String) -> Result<ArrayBuffer, ModuleError> {
46
+ files::read_file(path).await
47
+ }
48
+
49
+ async fn read_text_file(&self, path: String) -> Result<String, ModuleError> {
50
+ files::read_text_file(path).await
51
+ }
52
+
53
+ async fn write_file(
54
+ &self,
55
+ path: String,
56
+ data: ArrayBuffer,
57
+ flags: f64,
58
+ mode: f64,
59
+ ) -> Result<(), ModuleError> {
60
+ files::write_file(path, data, flags, mode).await
61
+ }
62
+
63
+ async fn write_text_file(
64
+ &self,
65
+ path: String,
66
+ data: String,
67
+ flags: f64,
68
+ mode: f64,
69
+ ) -> Result<(), ModuleError> {
70
+ files::write_text_file(path, data, flags, mode).await
71
+ }
72
+
73
+ async fn readdir(&self, path: String, recursive: bool) -> Result<Vec<DirEntry>, ModuleError> {
74
+ dirs::readdir(path, recursive).await
75
+ }
76
+
77
+ async fn stat(&self, path: String, follow_symlinks: bool) -> Result<StatResult, ModuleError> {
78
+ files::stat(path, follow_symlinks).await
79
+ }
80
+
81
+ async fn mkdir(&self, path: String, recursive: bool, mode: f64) -> Result<(), ModuleError> {
82
+ dirs::mkdir(path, recursive, mode).await
83
+ }
84
+
85
+ async fn rm(&self, path: String, recursive: bool, force: bool) -> Result<(), ModuleError> {
86
+ dirs::rm(path, recursive, force).await
87
+ }
88
+
89
+ async fn rename(&self, from: String, to: String) -> Result<(), ModuleError> {
90
+ files::rename(from, to).await
91
+ }
92
+
93
+ async fn copy(&self, from: String, to: String, recursive: bool) -> Result<(), ModuleError> {
94
+ files::copy(from, to, recursive).await
95
+ }
96
+
97
+ async fn access(&self, path: String, mode: f64) -> Result<(), ModuleError> {
98
+ links::access(path, mode)
99
+ }
100
+
101
+ async fn realpath(&self, path: String) -> Result<String, ModuleError> {
102
+ links::realpath(path)
103
+ }
104
+
105
+ async fn readlink(&self, path: String) -> Result<String, ModuleError> {
106
+ links::readlink(path)
107
+ }
108
+
109
+ async fn symlink(
110
+ &self,
111
+ target: String,
112
+ path: String,
113
+ kind: Option<String>,
114
+ ) -> Result<(), ModuleError> {
115
+ links::symlink(target, path, kind.as_deref())
116
+ }
117
+
118
+ async fn truncate(&self, path: String, length: f64) -> Result<(), ModuleError> {
119
+ files::truncate(path, length).await
120
+ }
121
+
122
+ async fn utimes(&self, path: String, atime_ms: f64, mtime_ms: f64) -> Result<(), ModuleError> {
123
+ files::utimes(path, atime_ms, mtime_ms).await
124
+ }
125
+
126
+ async fn chmod(&self, path: String, mode: f64) -> Result<(), ModuleError> {
127
+ files::chmod(path, mode).await
128
+ }
129
+
130
+ async fn mkdtemp(&self, prefix: String) -> Result<String, ModuleError> {
131
+ dirs::mkdtemp(prefix).await
132
+ }
133
+
134
+ async fn open(&self, path: String, flags: f64, mode: f64) -> Result<f64, ModuleError> {
135
+ handles::open(self, path, flags, mode)
136
+ }
137
+
138
+ async fn read(&self, fd: f64, length: f64, position: f64) -> Result<ArrayBuffer, ModuleError> {
139
+ handles::read(self, fd, length, position)
140
+ }
141
+
142
+ async fn write(&self, fd: f64, data: ArrayBuffer, position: f64) -> Result<f64, ModuleError> {
143
+ handles::write(self, fd, data, position)
144
+ }
145
+
146
+ async fn fstat(&self, fd: f64) -> Result<StatResult, ModuleError> {
147
+ handles::fstat(self, fd)
148
+ }
149
+
150
+ async fn fsync(&self, fd: f64) -> Result<(), ModuleError> {
151
+ handles::fsync(self, fd)
152
+ }
153
+
154
+ async fn ftruncate(&self, fd: f64, length: f64) -> Result<(), ModuleError> {
155
+ handles::ftruncate(self, fd, length)
156
+ }
157
+
158
+ async fn close(&self, fd: f64) -> Result<(), ModuleError> {
159
+ handles::close(self, fd)
160
+ }
161
+
162
+ async fn show_open_dialog(
163
+ &self,
164
+ options: OpenDialogOptions,
165
+ ) -> Result<Vec<String>, ModuleError> {
166
+ dialogs::show_open(options)
167
+ }
168
+
169
+ async fn show_save_dialog(
170
+ &self,
171
+ options: SaveDialogOptions,
172
+ ) -> Result<Option<String>, ModuleError> {
173
+ dialogs::show_save(options)
174
+ }
175
+
176
+ async fn reveal(&self, path: String) -> Result<(), ModuleError> {
177
+ dialogs::reveal(path)
178
+ }
179
+
180
+ async fn open_external(&self, path: String) -> Result<(), ModuleError> {
181
+ dialogs::open_external(path)
182
+ }
183
+
184
+ async fn watch(&self, path: String, recursive: bool) -> Result<f64, ModuleError> {
185
+ watching::watch(self, path, recursive)
186
+ }
187
+
188
+ async fn download(
189
+ &self,
190
+ url: String,
191
+ path: String,
192
+ options: DownloadOptions,
193
+ ) -> Result<DownloadResult, ModuleError> {
194
+ download::download(self, url, path, options).await
195
+ }
196
+
197
+ async fn upload(
198
+ &self,
199
+ url: String,
200
+ path: String,
201
+ options: UploadOptions,
202
+ ) -> Result<UploadResult, ModuleError> {
203
+ upload::upload(self, url, path, options).await
204
+ }
205
+
206
+ fn cancel_upload(&self, id: f64) -> Result<bool, ModuleError> {
207
+ Ok(upload::cancel(self, id))
208
+ }
209
+
210
+ fn cancel_download(&self, id: f64) -> Result<bool, ModuleError> {
211
+ Ok(download::cancel(self, id))
212
+ }
213
+
214
+ fn unwatch(&self, id: f64) {
215
+ watching::unwatch(self, id);
216
+ }
217
+
218
+ fn read_file_sync(&self, path: String) -> Result<ArrayBuffer, ModuleError> {
219
+ files::read_file_sync(path)
220
+ }
221
+
222
+ fn read_text_file_sync(&self, path: String) -> Result<String, ModuleError> {
223
+ files::read_text_file_sync(path)
224
+ }
225
+
226
+ fn write_file_sync(
227
+ &self,
228
+ path: String,
229
+ data: ArrayBuffer,
230
+ flags: f64,
231
+ mode: f64,
232
+ ) -> Result<bool, ModuleError> {
233
+ files::write_file_sync(path, data, flags, mode)
234
+ }
235
+
236
+ fn write_text_file_sync(
237
+ &self,
238
+ path: String,
239
+ data: String,
240
+ flags: f64,
241
+ mode: f64,
242
+ ) -> Result<bool, ModuleError> {
243
+ files::write_text_file_sync(path, data, flags, mode)
244
+ }
245
+
246
+ fn stat_sync(
247
+ &self,
248
+ path: String,
249
+ follow_symlinks: bool,
250
+ ) -> Result<Option<StatResult>, ModuleError> {
251
+ files::stat_sync(path, follow_symlinks)
252
+ }
253
+
254
+ fn exists_sync(&self, path: String) -> Result<bool, ModuleError> {
255
+ files::exists_sync(path)
256
+ }
257
+
258
+ fn readdir_sync(&self, path: String, recursive: bool) -> Result<Vec<DirEntry>, ModuleError> {
259
+ dirs::readdir_sync(path, recursive)
260
+ }
261
+
262
+ fn mkdir_sync(&self, path: String, recursive: bool, mode: f64) -> Result<bool, ModuleError> {
263
+ dirs::mkdir_sync(path, recursive, mode)
264
+ }
265
+
266
+ fn rm_sync(&self, path: String, recursive: bool, force: bool) -> Result<bool, ModuleError> {
267
+ dirs::rm_sync(path, recursive, force)
268
+ }
269
+
270
+ fn rename_sync(&self, from: String, to: String) -> Result<bool, ModuleError> {
271
+ files::rename_sync(from, to)
272
+ }
273
+
274
+ fn copy_file_sync(&self, from: String, to: String) -> Result<bool, ModuleError> {
275
+ files::copy_file_sync(from, to)
276
+ }
277
+
278
+ fn get_paths(&self) -> Result<KnownPaths, ModuleError> {
279
+ paths::known_paths()
280
+ }
281
+ }
282
+
283
+ /// Called by the app's autolink shim, which keeps these modules linked in.
284
+ pub fn rn_gpui_register() {
285
+ rn_gpui_module::register(native_fs_module::<RNFsModule>());
286
+ }
@@ -0,0 +1,336 @@
1
+ //! Links and path checks: `access`, `realpath`, `readlink` and `symlink`.
2
+ //!
3
+ //! These are the calls whose exact meaning differs most between the platforms,
4
+ //! so each one is written against the platform's own primitive rather than
5
+ //! approximated from `std::fs` where that would change the answer.
6
+
7
+ use std::path::Path;
8
+ use std::path::PathBuf;
9
+
10
+ use rn_gpui_module::ModuleError;
11
+
12
+ use crate::errors;
13
+ use crate::module::access;
14
+
15
+ pub fn access(path: String, mode: f64) -> Result<(), ModuleError> {
16
+ let mode = access_mode(mode)?;
17
+ platform_access(&path, mode)
18
+ }
19
+
20
+ pub fn realpath(path: String) -> Result<String, ModuleError> {
21
+ let resolved = dunce::canonicalize(&path).map_err(|error| errors::from_io(error, &path))?;
22
+ // A `String` crosses the bridge as UTF-8, so a path that is not valid UTF-8
23
+ // has no faithful representation. Lossy-converting would hand JS a path it
24
+ // cannot open, which is worse than failing: `EINVAL` says the argument could
25
+ // not be represented rather than that the path does not exist.
26
+ resolved
27
+ .into_os_string()
28
+ .into_string()
29
+ .map_err(|unrepresentable| {
30
+ errors::invalid(
31
+ "EINVAL",
32
+ format!(
33
+ "EINVAL: invalid argument, path is not valid UTF-8, {}",
34
+ Path::new(&unrepresentable).display()
35
+ ),
36
+ )
37
+ })
38
+ }
39
+
40
+ pub fn readlink(path: String) -> Result<String, ModuleError> {
41
+ let target = std::fs::read_link(&path).map_err(|error| errors::from_io(error, &path))?;
42
+ // As with `realpath`: the target is returned exactly as the file system
43
+ // stores it, and a target that is not UTF-8 cannot be a `String`.
44
+ target
45
+ .into_os_string()
46
+ .into_string()
47
+ .map_err(|unrepresentable| {
48
+ errors::invalid(
49
+ "EINVAL",
50
+ format!(
51
+ "EINVAL: invalid argument, link target is not valid UTF-8, {}",
52
+ Path::new(&unrepresentable).display()
53
+ ),
54
+ )
55
+ })
56
+ }
57
+
58
+ pub fn symlink(target: String, path: String, kind: Option<&str>) -> Result<(), ModuleError> {
59
+ platform_symlink(&target, &path, kind)
60
+ }
61
+
62
+ /// Validate the bitmask Node numbers, returning it unchanged.
63
+ ///
64
+ /// The caller gets Node's own bits back, not `access(2)`'s: `libc` 0.2 defines
65
+ /// `R_OK`, `W_OK` and `X_OK` only for Unix, so a Windows build that named them
66
+ /// failed with "cannot find value `R_OK` in crate `libc`". The translation to
67
+ /// `access(2)` happens in `platform_access`, which is already Unix-only, and the
68
+ /// Windows path reads the same Node bits directly.
69
+ ///
70
+ /// `F_OK` is 0: the mode is not a flag set, and asking for nothing means "does
71
+ /// it exist".
72
+ fn access_mode(mode: f64) -> Result<i32, ModuleError> {
73
+ if !mode.is_finite() || mode < 0.0 || mode.fract() != 0.0 {
74
+ return Err(errors::invalid(
75
+ "EINVAL",
76
+ format!("EINVAL: invalid argument, mode {mode}"),
77
+ ));
78
+ }
79
+ let requested = mode as i32;
80
+ let known = access::F_OK | access::R_OK | access::W_OK | access::X_OK;
81
+ if requested & !known != 0 {
82
+ return Err(errors::invalid(
83
+ "EINVAL",
84
+ format!("EINVAL: invalid argument, mode {requested}"),
85
+ ));
86
+ }
87
+ Ok(requested)
88
+ }
89
+
90
+ /// `access(2)` is the only way to get Node's semantics — it asks the kernel about
91
+ /// *this* process's real uid and gid, including supplementary groups — so the
92
+ /// Unix implementation calls it rather than inspecting the mode bits, which
93
+ /// would give a different answer for a file the caller does not own.
94
+ #[cfg(unix)]
95
+ fn platform_access(path: &str, requested: i32) -> Result<(), ModuleError> {
96
+ use std::ffi::CString;
97
+ use std::os::unix::ffi::OsStrExt as _;
98
+
99
+ // `access(2)` uses the same values Node does for these three, but the
100
+ // translation is spelled out so a platform whose constants differ still asks
101
+ // the right question. This is the only place the `libc` constants are named.
102
+ let mut mask = libc::F_OK;
103
+ if requested & access::R_OK != 0 {
104
+ mask |= libc::R_OK;
105
+ }
106
+ if requested & access::W_OK != 0 {
107
+ mask |= libc::W_OK;
108
+ }
109
+ if requested & access::X_OK != 0 {
110
+ mask |= libc::X_OK;
111
+ }
112
+
113
+ // A path with an interior NUL cannot be passed to the C API at all; that is
114
+ // an invalid argument rather than a file-system failure.
115
+ let name = CString::new(std::ffi::OsStr::new(path).as_bytes()).map_err(|_| {
116
+ errors::invalid(
117
+ "EINVAL",
118
+ format!("EINVAL: invalid argument, path contains a null byte, {path}"),
119
+ )
120
+ })?;
121
+ // `access` reports failure by return value and sets `errno`, so the error is
122
+ // built from `last_os_error` and then mapped like any other.
123
+ // SAFETY: `name` is a NUL-terminated string that outlives the call.
124
+ let result = unsafe { libc::access(name.as_ptr(), mask) };
125
+ if result == 0 {
126
+ Ok(())
127
+ } else {
128
+ Err(errors::from_io(std::io::Error::last_os_error(), path))
129
+ }
130
+ }
131
+
132
+ /// Windows has no `access(2)`, so the checks are approximated from what the
133
+ /// platform does offer: `F_OK` and `R_OK` are a `metadata()` call (the read bit
134
+ /// is not expressible, and a file the process can stat is one it can normally
135
+ /// read), `W_OK` tries to open the file for writing, and `X_OK` uses the
136
+ /// executable extensions the shell recognises, because Windows has no execute
137
+ /// bit to test.
138
+ #[cfg(not(unix))]
139
+ fn platform_access(path: &str, requested: i32) -> Result<(), ModuleError> {
140
+ // `F_OK` asks only that the path resolves; `metadata` follows links, which
141
+ // matches `access`'s "does the name lead to something".
142
+ std::fs::metadata(path).map_err(|error| errors::from_io(error, path))?;
143
+ if requested & access::W_OK != 0 {
144
+ std::fs::OpenOptions::new()
145
+ .write(true)
146
+ .open(path)
147
+ .map_err(|error| errors::from_io(error, path))?;
148
+ }
149
+ if requested & access::X_OK != 0 && !is_executable(Path::new(path)) {
150
+ return Err(errors::invalid(
151
+ "EACCES",
152
+ format!("EACCES: permission denied, {path}"),
153
+ ));
154
+ }
155
+ Ok(())
156
+ }
157
+
158
+ /// The extensions `PATHEXT` names, which is how Windows decides a file is a
159
+ /// program. A directory is executable for `access`'s purposes because the caller
160
+ /// can `chdir` into it.
161
+ #[cfg(not(unix))]
162
+ fn is_executable(path: &Path) -> bool {
163
+ if path.is_dir() {
164
+ return true;
165
+ }
166
+ let extensions = std::env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".to_string());
167
+ let Some(extension) = path.extension().and_then(|value| value.to_str()) else {
168
+ return false;
169
+ };
170
+ let extension = format!(".{}", extension.to_ascii_uppercase());
171
+ extensions
172
+ .split(';')
173
+ .any(|candidate| candidate.eq_ignore_ascii_case(&extension))
174
+ }
175
+
176
+ /// Unix ignores the link kind: `symlink(2)` does not have one, and the target is
177
+ /// resolved lazily on every traversal, so a link to a directory works without
178
+ /// being declared as one.
179
+ #[cfg(unix)]
180
+ fn platform_symlink(target: &str, path: &str, kind: Option<&str>) -> Result<(), ModuleError> {
181
+ let _ = kind;
182
+ std::os::unix::fs::symlink(target, path).map_err(|error| errors::from_io(error, path))
183
+ }
184
+
185
+ /// Windows needs a different call for a file link and a directory link, and it
186
+ /// needs the developer-mode privilege (or SeCreateSymbolicLinkPrivilege) for
187
+ /// either. The design says to report the OS error rather than fall back to
188
+ /// copying, so a caller without the privilege sees the real reason.
189
+ ///
190
+ /// The kind decides which call, following Node's rules. It cannot be inferred
191
+ /// from the target alone: a link may name a path that does not exist yet, and
192
+ /// `metadata` would fail on exactly the case that matters. `'junction'` gets
193
+ /// `symlink_dir` — a directory symlink rather than a true NTFS junction, which
194
+ /// needs a different API and an absolute target; both are traversable, which is
195
+ /// what a caller wants.
196
+ #[cfg(not(unix))]
197
+ fn platform_symlink(target: &str, path: &str, kind: Option<&str>) -> Result<(), ModuleError> {
198
+ let directory = match kind {
199
+ Some("dir") | Some("junction") => true,
200
+ Some("file") => false,
201
+ _ => {
202
+ // Node's rule when no type is given: resolve the target relative to
203
+ // the link's parent — the shell does the same — and take the answer
204
+ // from what is there.
205
+ let resolved = match Path::new(path).parent() {
206
+ Some(parent) => parent.join(target),
207
+ None => PathBuf::from(target),
208
+ };
209
+ resolved.is_dir()
210
+ }
211
+ };
212
+ let result = if directory {
213
+ std::os::windows::fs::symlink_dir(target, path)
214
+ } else {
215
+ std::os::windows::fs::symlink_file(target, path)
216
+ };
217
+ result.map_err(|error| errors::from_io(error, path))
218
+ }
219
+
220
+ #[cfg(test)]
221
+ mod tests {
222
+ use super::*;
223
+ use tempfile::TempDir;
224
+
225
+ fn tempdir() -> TempDir {
226
+ TempDir::new().expect("a temporary directory")
227
+ }
228
+
229
+ fn text(path: &Path) -> String {
230
+ path.to_string_lossy().into_owned()
231
+ }
232
+
233
+ #[test]
234
+ fn access_finds_a_present_path_and_reports_a_missing_one() {
235
+ let dir = tempdir();
236
+ let present = dir.path().join("here.txt");
237
+ std::fs::write(&present, b"x").expect("write the fixture file");
238
+ let absent = dir.path().join("gone.txt");
239
+
240
+ access(text(&present), f64::from(access::F_OK)).expect("a present path exists");
241
+ access(text(&present), f64::from(access::R_OK | access::W_OK))
242
+ .expect("a file we just wrote is readable and writable");
243
+
244
+ let error = access(text(&absent), f64::from(access::F_OK)).expect_err("a missing path");
245
+ assert_eq!(error.code.as_deref(), Some("ENOENT"));
246
+ }
247
+
248
+ #[test]
249
+ fn access_rejects_a_mode_node_does_not_define() {
250
+ let dir = tempdir();
251
+ let error = access(text(dir.path()), 8.0).expect_err("an unknown bit");
252
+ assert_eq!(error.code.as_deref(), Some("EINVAL"));
253
+ }
254
+
255
+ /// `dunce::canonicalize` resolves the whole chain, so the result is the real
256
+ /// file rather than the link. Windows is skipped: creating a symlink there
257
+ /// needs developer mode, which a CI runner may not have.
258
+ #[cfg(unix)]
259
+ #[test]
260
+ fn realpath_resolves_a_symlink() {
261
+ let dir = tempdir();
262
+ let file = dir.path().join("real.txt");
263
+ std::fs::write(&file, b"x").expect("write the fixture file");
264
+ let link = dir.path().join("link.txt");
265
+ std::os::unix::fs::symlink(&file, &link).expect("create the link");
266
+
267
+ assert_eq!(
268
+ realpath(text(&link)).expect("realpath"),
269
+ realpath(text(&file)).expect("realpath")
270
+ );
271
+ assert_eq!(
272
+ realpath(text(&file)).expect("realpath"),
273
+ dunce::canonicalize(&file)
274
+ .expect("canonical")
275
+ .to_string_lossy()
276
+ );
277
+ }
278
+
279
+ #[test]
280
+ fn realpath_of_a_missing_path_is_enoent() {
281
+ let dir = tempdir();
282
+ let error = realpath(text(&dir.path().join("gone"))).expect_err("missing");
283
+ assert_eq!(error.code.as_deref(), Some("ENOENT"));
284
+ }
285
+
286
+ /// The link is read, not followed: the target comes back as it was stored,
287
+ /// relative or absolute.
288
+ #[cfg(unix)]
289
+ #[test]
290
+ fn readlink_returns_the_stored_target() {
291
+ let dir = tempdir();
292
+ let link = dir.path().join("link.txt");
293
+ std::os::unix::fs::symlink("some/relative/target", &link).expect("create the link");
294
+ assert_eq!(
295
+ readlink(text(&link)).expect("readlink"),
296
+ "some/relative/target"
297
+ );
298
+
299
+ let file = dir.path().join("real.txt");
300
+ std::fs::write(&file, b"x").expect("write the fixture file");
301
+ let error = readlink(text(&file)).expect_err("a plain file is not a link");
302
+ assert_eq!(error.code.as_deref(), Some("EINVAL"));
303
+ }
304
+
305
+ /// Windows is skipped because creating a symlink there needs developer mode;
306
+ /// the OS error would be the correct result, not a test failure.
307
+ #[cfg(unix)]
308
+ #[test]
309
+ fn symlink_round_trips_through_readlink() {
310
+ let dir = tempdir();
311
+ let target = dir.path().join("target.txt");
312
+ std::fs::write(&target, b"x").expect("write the fixture file");
313
+ let link = dir.path().join("link.txt");
314
+
315
+ symlink(text(&target), text(&link), None).expect("symlink");
316
+ assert_eq!(readlink(text(&link)).expect("readlink"), text(&target));
317
+ assert_eq!(std::fs::read(&link).expect("through the link"), b"x");
318
+ }
319
+
320
+ #[test]
321
+ fn symlinking_onto_an_existing_path_is_eexist() {
322
+ let dir = tempdir();
323
+ let one = dir.path().join("one.txt");
324
+ let two = dir.path().join("two.txt");
325
+ std::fs::write(&one, b"1").expect("write");
326
+ std::fs::write(&two, b"2").expect("write");
327
+
328
+ let error = symlink(text(&one), text(&two), None).expect_err("the name is taken");
329
+ #[cfg(unix)]
330
+ assert_eq!(error.code.as_deref(), Some("EEXIST"));
331
+ // Windows reports the same collision, but through a Win32 code whose
332
+ // kind is "already exists"; only the presence of an error is portable.
333
+ #[cfg(not(unix))]
334
+ assert!(error.code.is_some());
335
+ }
336
+ }