@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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +36 -2
- package/desktop/Cargo.toml +29 -0
- package/desktop/build.rs +3 -0
- package/desktop/src/dialogs.rs +394 -0
- package/desktop/src/dirs.rs +363 -0
- package/desktop/src/download.rs +329 -0
- package/desktop/src/errors.rs +149 -0
- package/desktop/src/files.rs +640 -0
- package/desktop/src/handles.rs +697 -0
- package/desktop/src/lib.rs +286 -0
- package/desktop/src/links.rs +336 -0
- package/desktop/src/module.rs +192 -0
- package/desktop/src/paths.rs +231 -0
- package/desktop/src/upload.rs +237 -0
- package/desktop/src/watching.rs +392 -0
- package/package.json +38 -4
- package/react-native.config.js +9 -0
- package/src/NativeFs.ts +235 -0
- package/src/constants.ts +43 -0
- package/src/download.ts +110 -0
- package/src/index.ts +788 -0
- package/src/promises.ts +771 -0
- package/src/shared.ts +539 -0
- package/src/stats.ts +196 -0
- package/src/upload.ts +97 -0
- package/src/watch.ts +141 -0
|
@@ -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
|
+
}
|