@react-native-gpui/fs 0.0.0-stage → 0.0.1-alpha.0
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 +8 -0
- package/LICENSE +21 -0
- package/README.md +36 -2
- package/desktop/Cargo.lock +1989 -0
- package/desktop/Cargo.toml +25 -0
- package/desktop/build.rs +3 -0
- package/desktop/src/dialogs.rs +394 -0
- package/desktop/src/dirs.rs +363 -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 +258 -0
- package/desktop/src/links.rs +336 -0
- package/desktop/src/module.rs +187 -0
- package/desktop/src/paths.rs +231 -0
- package/desktop/src/watching.rs +392 -0
- package/package.json +37 -4
- package/react-native.config.js +9 -0
- package/src/NativeFs.ts +162 -0
- package/src/constants.ts +43 -0
- package/src/index.ts +769 -0
- package/src/promises.ts +771 -0
- package/src/shared.ts +539 -0
- package/src/stats.ts +196 -0
- package/src/watch.ts +141 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
//! Listing, creating and removing directories, and `mkdtemp`.
|
|
2
|
+
//!
|
|
3
|
+
//! `readdir` returns every entry as a plain `{name, kind, parentPath}` object,
|
|
4
|
+
//! which the JS layer turns into `Dirent` instances; there is no per-directory
|
|
5
|
+
//! cursor to hold open because the whole listing crosses the bridge at once.
|
|
6
|
+
|
|
7
|
+
use std::fs;
|
|
8
|
+
use std::path::{Path, PathBuf};
|
|
9
|
+
|
|
10
|
+
use rn_gpui_module::ModuleError;
|
|
11
|
+
|
|
12
|
+
use crate::errors;
|
|
13
|
+
use crate::module::kind;
|
|
14
|
+
use crate::DirEntry;
|
|
15
|
+
|
|
16
|
+
pub async fn readdir(path: String, recursive: bool) -> Result<Vec<DirEntry>, ModuleError> {
|
|
17
|
+
readdir_sync(path, recursive)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub async fn mkdir(path: String, recursive: bool, mode: f64) -> Result<(), ModuleError> {
|
|
21
|
+
create_dir(&path, recursive, mode)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pub async fn rm(path: String, recursive: bool, force: bool) -> Result<(), ModuleError> {
|
|
25
|
+
remove(&path, recursive, force)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pub async fn mkdtemp(prefix: String) -> Result<String, ModuleError> {
|
|
29
|
+
let directory = tempfile::Builder::new()
|
|
30
|
+
.prefix(&prefix)
|
|
31
|
+
.tempdir_in(std::env::temp_dir())
|
|
32
|
+
.map_err(|error| errors::from_io(error, &prefix))?;
|
|
33
|
+
// The caller asked for a directory to keep, so it must outlive this frame;
|
|
34
|
+
// dropping the guard would delete it again.
|
|
35
|
+
Ok(path_string(directory.keep()))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub fn readdir_sync(path: String, recursive: bool) -> Result<Vec<DirEntry>, ModuleError> {
|
|
39
|
+
let entries = list(Path::new(&path))?;
|
|
40
|
+
if !recursive {
|
|
41
|
+
return Ok(entries);
|
|
42
|
+
}
|
|
43
|
+
let mut collected = entries;
|
|
44
|
+
// Depth-first, appending each level as it is reached. Iterative rather than
|
|
45
|
+
// recursive: a deep tree should cost heap, not stack.
|
|
46
|
+
let mut pending: Vec<PathBuf> = collected
|
|
47
|
+
.iter()
|
|
48
|
+
.filter(|entry| entry.kind == kind::DIRECTORY)
|
|
49
|
+
.map(|entry| Path::new(&entry.parent_path).join(&entry.name))
|
|
50
|
+
.collect();
|
|
51
|
+
while let Some(directory) = pending.pop() {
|
|
52
|
+
let children = list(&directory)?;
|
|
53
|
+
pending.extend(
|
|
54
|
+
children
|
|
55
|
+
.iter()
|
|
56
|
+
.filter(|entry| entry.kind == kind::DIRECTORY)
|
|
57
|
+
.map(|entry| Path::new(&entry.parent_path).join(&entry.name)),
|
|
58
|
+
);
|
|
59
|
+
collected.extend(children);
|
|
60
|
+
}
|
|
61
|
+
Ok(collected)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pub fn mkdir_sync(path: String, recursive: bool, mode: f64) -> Result<bool, ModuleError> {
|
|
65
|
+
create_dir(&path, recursive, mode)?;
|
|
66
|
+
Ok(true)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
pub fn rm_sync(path: String, recursive: bool, force: bool) -> Result<bool, ModuleError> {
|
|
70
|
+
remove(&path, recursive, force)?;
|
|
71
|
+
Ok(true)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// The direct children of `path`, in the order the file system lists them.
|
|
75
|
+
///
|
|
76
|
+
/// `DirEntry::file_type` does not follow symbolic links, so a link is reported
|
|
77
|
+
/// as a link rather than as whatever it points at.
|
|
78
|
+
fn list(path: &Path) -> Result<Vec<DirEntry>, ModuleError> {
|
|
79
|
+
let name = path_string(path.to_path_buf());
|
|
80
|
+
let iterator = fs::read_dir(path).map_err(|error| errors::from_io(error, &name))?;
|
|
81
|
+
let mut entries = Vec::new();
|
|
82
|
+
for entry in iterator {
|
|
83
|
+
let entry = entry.map_err(|error| errors::from_io(error, &name))?;
|
|
84
|
+
let file_type = entry
|
|
85
|
+
.file_type()
|
|
86
|
+
.map_err(|error| errors::from_io(error, &name))?;
|
|
87
|
+
entries.push(DirEntry {
|
|
88
|
+
name: entry.file_name().to_string_lossy().into_owned(),
|
|
89
|
+
kind: entry_kind(&file_type),
|
|
90
|
+
parent_path: name.clone(),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
Ok(entries)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn entry_kind(file_type: &fs::FileType) -> f64 {
|
|
97
|
+
if file_type.is_file() {
|
|
98
|
+
kind::FILE
|
|
99
|
+
} else if file_type.is_dir() {
|
|
100
|
+
kind::DIRECTORY
|
|
101
|
+
} else if file_type.is_symlink() {
|
|
102
|
+
kind::SYMLINK
|
|
103
|
+
} else {
|
|
104
|
+
kind::OTHER
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fn create_dir(path: &str, recursive: bool, mode: f64) -> Result<(), ModuleError> {
|
|
109
|
+
if recursive {
|
|
110
|
+
// `create_dir_all` succeeds on an existing directory, which is what
|
|
111
|
+
// Node's `{recursive: true}` promises.
|
|
112
|
+
return fs::create_dir_all(path).map_err(|error| errors::from_io(error, path));
|
|
113
|
+
}
|
|
114
|
+
#[cfg(unix)]
|
|
115
|
+
{
|
|
116
|
+
use std::os::unix::fs::DirBuilderExt as _;
|
|
117
|
+
let mut builder = fs::DirBuilder::new();
|
|
118
|
+
builder.mode(crate::module::mode(mode));
|
|
119
|
+
builder
|
|
120
|
+
.create(path)
|
|
121
|
+
.map_err(|error| errors::from_io(error, path))
|
|
122
|
+
}
|
|
123
|
+
// Windows has no mode for a new directory, and `create_dir` would reject
|
|
124
|
+
// the `DirBuilder` extension that only exists on Unix.
|
|
125
|
+
#[cfg(not(unix))]
|
|
126
|
+
{
|
|
127
|
+
let _ = mode;
|
|
128
|
+
fs::create_dir(path).map_err(|error| errors::from_io(error, path))
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn remove(path: &str, recursive: bool, force: bool) -> Result<(), ModuleError> {
|
|
133
|
+
// `symlink_metadata`, so a link is removed as a link instead of acting on
|
|
134
|
+
// its target.
|
|
135
|
+
let metadata = match fs::symlink_metadata(path) {
|
|
136
|
+
Ok(metadata) => metadata,
|
|
137
|
+
Err(error) if force && errors::code_for(&error) == "ENOENT" => return Ok(()),
|
|
138
|
+
Err(error) => return Err(errors::from_io(error, path)),
|
|
139
|
+
};
|
|
140
|
+
if recursive && metadata.is_dir() {
|
|
141
|
+
return fs::remove_dir_all(path).map_err(|error| errors::from_io(error, path));
|
|
142
|
+
}
|
|
143
|
+
if metadata.is_dir() {
|
|
144
|
+
// `remove_dir` fails with ENOTEMPTY on Unix and a folder-not-empty error
|
|
145
|
+
// on Windows, which the mapping turns into the same code; removing a
|
|
146
|
+
// file with it reports what Node calls ENOTDIR.
|
|
147
|
+
fs::remove_dir(path).map_err(|error| errors::from_io(error, path))
|
|
148
|
+
} else {
|
|
149
|
+
fs::remove_file(path).map_err(|error| errors::from_io(error, path))
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fn path_string(path: PathBuf) -> String {
|
|
154
|
+
path.to_string_lossy().into_owned()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[cfg(test)]
|
|
158
|
+
mod tests {
|
|
159
|
+
use super::*;
|
|
160
|
+
use tempfile::TempDir;
|
|
161
|
+
|
|
162
|
+
/// The async wrappers never suspend, so polling once with a no-op waker is
|
|
163
|
+
/// enough and keeps the tests independent of a runtime.
|
|
164
|
+
fn block_on<F: std::future::Future>(future: F) -> F::Output {
|
|
165
|
+
let mut future = std::pin::pin!(future);
|
|
166
|
+
let waker = std::task::Waker::noop();
|
|
167
|
+
let mut context = std::task::Context::from_waker(waker);
|
|
168
|
+
// One poll, no loop: these helpers do their work inline, so a `Pending`
|
|
169
|
+
// would mean the helper is not synchronous after all, which the test
|
|
170
|
+
// should fail loudly on rather than spin.
|
|
171
|
+
match future.as_mut().poll(&mut context) {
|
|
172
|
+
std::task::Poll::Ready(value) => value,
|
|
173
|
+
std::task::Poll::Pending => panic!("the wrappers are synchronous"),
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn tempdir() -> TempDir {
|
|
178
|
+
TempDir::new().expect("a temporary directory")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fn text(path: &Path) -> String {
|
|
182
|
+
path.to_string_lossy().into_owned()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// The listed paths, with separators normalised to `/`.
|
|
186
|
+
///
|
|
187
|
+
/// The entries themselves carry the platform separator, which on Windows is
|
|
188
|
+
/// `\`; building the expected path with `/` and comparing would test the
|
|
189
|
+
/// separator rather than the listing.
|
|
190
|
+
fn names(entries: &[DirEntry]) -> Vec<String> {
|
|
191
|
+
let mut names: Vec<String> = entries
|
|
192
|
+
.iter()
|
|
193
|
+
.map(|entry| format!("{}/{}", entry.parent_path, entry.name).replace('\\', "/"))
|
|
194
|
+
.collect();
|
|
195
|
+
names.sort();
|
|
196
|
+
names
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fn tree(root: &Path) {
|
|
200
|
+
fs::create_dir_all(root.join("nested").join("deep")).expect("nested");
|
|
201
|
+
fs::write(root.join("top.txt"), b"top").expect("seed");
|
|
202
|
+
fs::write(root.join("nested").join("middle.txt"), b"middle").expect("seed");
|
|
203
|
+
fs::write(root.join("nested").join("deep").join("leaf.txt"), b"leaf").expect("seed");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#[test]
|
|
207
|
+
fn readdir_lists_direct_children_only() {
|
|
208
|
+
let dir = tempdir();
|
|
209
|
+
tree(dir.path());
|
|
210
|
+
|
|
211
|
+
let entries = readdir_sync(text(dir.path()), false).expect("readdir");
|
|
212
|
+
// `names` normalises separators, so the expected paths are built from a
|
|
213
|
+
// normalised root rather than the platform one.
|
|
214
|
+
let root = text(dir.path()).replace('\\', "/");
|
|
215
|
+
assert_eq!(
|
|
216
|
+
names(&entries),
|
|
217
|
+
vec![format!("{root}/nested"), format!("{root}/top.txt")]
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
let top = entries
|
|
221
|
+
.iter()
|
|
222
|
+
.find(|entry| entry.name == "top.txt")
|
|
223
|
+
.expect("top.txt");
|
|
224
|
+
assert_eq!(top.kind, kind::FILE);
|
|
225
|
+
assert_eq!(top.parent_path, text(dir.path()));
|
|
226
|
+
let nested = entries
|
|
227
|
+
.iter()
|
|
228
|
+
.find(|entry| entry.name == "nested")
|
|
229
|
+
.expect("nested");
|
|
230
|
+
assert_eq!(nested.kind, kind::DIRECTORY);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#[test]
|
|
234
|
+
fn readdir_recursive_reaches_every_descendant() {
|
|
235
|
+
let dir = tempdir();
|
|
236
|
+
tree(dir.path());
|
|
237
|
+
|
|
238
|
+
let entries = block_on(readdir(text(dir.path()), true)).expect("readdir");
|
|
239
|
+
// Two entries per directory, three directories deep.
|
|
240
|
+
assert_eq!(entries.len(), 5);
|
|
241
|
+
let listed = names(&entries);
|
|
242
|
+
let root = text(dir.path()).replace('\\', "/");
|
|
243
|
+
assert!(listed.contains(&format!("{root}/nested/deep/leaf.txt")));
|
|
244
|
+
assert!(listed.contains(&format!("{root}/nested/deep")));
|
|
245
|
+
|
|
246
|
+
let deep = entries
|
|
247
|
+
.iter()
|
|
248
|
+
.find(|entry| entry.name == "deep")
|
|
249
|
+
.expect("deep");
|
|
250
|
+
assert_eq!(
|
|
251
|
+
deep.parent_path,
|
|
252
|
+
text(&dir.path().join("nested")),
|
|
253
|
+
"parentPath is the containing directory"
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[cfg(unix)]
|
|
258
|
+
#[test]
|
|
259
|
+
fn readdir_reports_a_link_as_a_link() {
|
|
260
|
+
let dir = tempdir();
|
|
261
|
+
let target = dir.path().join("target.txt");
|
|
262
|
+
fs::write(&target, b"x").expect("seed");
|
|
263
|
+
std::os::unix::fs::symlink(&target, dir.path().join("link.txt")).expect("symlink");
|
|
264
|
+
|
|
265
|
+
let entries = readdir_sync(text(dir.path()), false).expect("readdir");
|
|
266
|
+
let link = entries
|
|
267
|
+
.iter()
|
|
268
|
+
.find(|entry| entry.name == "link.txt")
|
|
269
|
+
.expect("link.txt");
|
|
270
|
+
assert_eq!(link.kind, kind::SYMLINK);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#[test]
|
|
274
|
+
fn mkdir_with_recursive_creates_the_chain_and_accepts_an_existing_dir() {
|
|
275
|
+
let dir = tempdir();
|
|
276
|
+
let nested = dir.path().join("a").join("b").join("c");
|
|
277
|
+
|
|
278
|
+
block_on(mkdir(text(&nested), true, f64::from(0o755u32))).expect("mkdir");
|
|
279
|
+
assert!(nested.is_dir());
|
|
280
|
+
|
|
281
|
+
let again = mkdir_sync(text(&nested), true, f64::from(0o755u32)).expect("mkdir again");
|
|
282
|
+
assert!(again, "recursive mkdir succeeds when the path exists");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#[test]
|
|
286
|
+
fn mkdir_without_recursive_fails_with_eexist() {
|
|
287
|
+
let dir = tempdir();
|
|
288
|
+
let path = dir.path().join("single");
|
|
289
|
+
assert!(mkdir_sync(text(&path), false, f64::from(0o755u32)).expect("mkdir"));
|
|
290
|
+
assert!(path.is_dir());
|
|
291
|
+
|
|
292
|
+
let error =
|
|
293
|
+
block_on(mkdir(text(&path), false, f64::from(0o755u32))).expect_err("mkdir twice");
|
|
294
|
+
assert_eq!(error.code.as_deref(), Some("EEXIST"));
|
|
295
|
+
|
|
296
|
+
let missing_parent = text(&dir.path().join("no").join("such").join("dir"));
|
|
297
|
+
let error =
|
|
298
|
+
mkdir_sync(missing_parent, false, f64::from(0o755u32)).expect_err("missing parent");
|
|
299
|
+
assert_eq!(error.code.as_deref(), Some("ENOENT"));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#[test]
|
|
303
|
+
fn rm_force_ignores_a_missing_path() {
|
|
304
|
+
let dir = tempdir();
|
|
305
|
+
let missing = text(&dir.path().join("absent"));
|
|
306
|
+
|
|
307
|
+
block_on(rm(missing.clone(), false, true)).expect("rm force");
|
|
308
|
+
assert!(rm_sync(missing.clone(), true, true).expect("rm sync force"));
|
|
309
|
+
|
|
310
|
+
let error = block_on(rm(missing, false, false)).expect_err("rm without force");
|
|
311
|
+
assert_eq!(error.code.as_deref(), Some("ENOENT"));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
#[test]
|
|
315
|
+
fn rm_removes_a_file_and_a_tree() {
|
|
316
|
+
let dir = tempdir();
|
|
317
|
+
let file = dir.path().join("one.txt");
|
|
318
|
+
fs::write(&file, b"x").expect("seed");
|
|
319
|
+
block_on(rm(text(&file), false, false)).expect("rm file");
|
|
320
|
+
assert!(!file.exists());
|
|
321
|
+
|
|
322
|
+
let root = dir.path().join("tree");
|
|
323
|
+
tree(&root);
|
|
324
|
+
assert!(rm_sync(text(&root), true, false).expect("rm tree"));
|
|
325
|
+
assert!(!root.exists());
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#[test]
|
|
329
|
+
fn rm_without_recursive_refuses_a_non_empty_directory() {
|
|
330
|
+
let dir = tempdir();
|
|
331
|
+
let root = dir.path().join("tree");
|
|
332
|
+
tree(&root);
|
|
333
|
+
|
|
334
|
+
let error = block_on(rm(text(&root), false, false)).expect_err("non-empty");
|
|
335
|
+
assert_eq!(error.code.as_deref(), Some("ENOTEMPTY"));
|
|
336
|
+
assert!(root.exists());
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#[test]
|
|
340
|
+
fn mkdtemp_creates_a_random_directory_with_the_prefix() {
|
|
341
|
+
let created = block_on(mkdtemp("rn-gpui-fs-".to_string())).expect("mkdtemp");
|
|
342
|
+
let path = Path::new(&created);
|
|
343
|
+
let name = path
|
|
344
|
+
.file_name()
|
|
345
|
+
.expect("the returned path names a directory")
|
|
346
|
+
.to_string_lossy()
|
|
347
|
+
.into_owned();
|
|
348
|
+
|
|
349
|
+
assert!(name.starts_with("rn-gpui-fs-"), "name was {name}");
|
|
350
|
+
assert!(name.len() > "rn-gpui-fs-".len(), "the rest is random");
|
|
351
|
+
assert!(path.is_dir());
|
|
352
|
+
assert_eq!(path.parent(), Some(std::env::temp_dir().as_path()));
|
|
353
|
+
|
|
354
|
+
fs::remove_dir_all(path).expect("clean up");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
#[test]
|
|
358
|
+
fn readdir_of_a_missing_directory_is_enoent() {
|
|
359
|
+
let dir = tempdir();
|
|
360
|
+
let error = readdir_sync(text(&dir.path().join("absent")), false).expect_err("readdir");
|
|
361
|
+
assert_eq!(error.code.as_deref(), Some("ENOENT"));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
//! Node-compatible error mapping.
|
|
2
|
+
//!
|
|
3
|
+
//! Every failure a caller sees has to look like the one Node would have raised for
|
|
4
|
+
//! the same call: a `code` such as `ENOENT`, and a message in Node's format.
|
|
5
|
+
//! `std::io::Error` gives a kind and, on Unix, a raw errno; neither is a Node code,
|
|
6
|
+
//! so the translation lives here once rather than at each call site.
|
|
7
|
+
|
|
8
|
+
use rn_gpui_module::ModuleError;
|
|
9
|
+
|
|
10
|
+
/// The Node code for an `std::io::Error`.
|
|
11
|
+
///
|
|
12
|
+
/// The raw OS error is consulted before the kind because several distinct errnos
|
|
13
|
+
/// share a kind: `EEXIST` and `ENOTEMPTY` are both "already exists"-ish, and
|
|
14
|
+
/// `EACCES` versus `EPERM` matter to a caller deciding whether to try again.
|
|
15
|
+
pub fn code_for(error: &std::io::Error) -> &'static str {
|
|
16
|
+
#[cfg(unix)]
|
|
17
|
+
if let Some(errno) = error.raw_os_error() {
|
|
18
|
+
let code = match errno {
|
|
19
|
+
libc::ENOENT => "ENOENT",
|
|
20
|
+
libc::EACCES => "EACCES",
|
|
21
|
+
libc::EPERM => "EPERM",
|
|
22
|
+
libc::EEXIST => "EEXIST",
|
|
23
|
+
libc::ENOTDIR => "ENOTDIR",
|
|
24
|
+
libc::EISDIR => "EISDIR",
|
|
25
|
+
libc::ENOTEMPTY => "ENOTEMPTY",
|
|
26
|
+
libc::EINVAL => "EINVAL",
|
|
27
|
+
libc::ELOOP => "ELOOP",
|
|
28
|
+
libc::ENOSPC => "ENOSPC",
|
|
29
|
+
libc::EROFS => "EROFS",
|
|
30
|
+
libc::EMFILE => "EMFILE",
|
|
31
|
+
libc::ENAMETOOLONG => "ENAMETOOLONG",
|
|
32
|
+
// Windows' share-violation has no Unix equivalent; callers treat it
|
|
33
|
+
// as a permission problem, which is what Node's `EPERM` means there.
|
|
34
|
+
_ => return code_for_kind(error.kind()),
|
|
35
|
+
};
|
|
36
|
+
return code;
|
|
37
|
+
}
|
|
38
|
+
// Windows exposes `raw_os_error` too, but its values are Win32 codes rather
|
|
39
|
+
// than errnos, so the kind is the better signal there, with the few Win32
|
|
40
|
+
// codes whose kind is not enough named below.
|
|
41
|
+
#[cfg(not(unix))]
|
|
42
|
+
if let Some(code) = win32_code(error.raw_os_error()) {
|
|
43
|
+
return code;
|
|
44
|
+
}
|
|
45
|
+
code_for_kind(error.kind())
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// The Node code for a Win32 error whose `ErrorKind` is too coarse.
|
|
49
|
+
///
|
|
50
|
+
/// `ERROR_PRIVILEGE_NOT_HELD` (1314) has no kind of its own, so it would be `EIO`.
|
|
51
|
+
/// Creating a symbolic link without the privilege is the case that hits it, and
|
|
52
|
+
/// libuv, which Node is built on, reports it as `EPERM`.
|
|
53
|
+
#[cfg(not(unix))]
|
|
54
|
+
fn win32_code(raw: Option<i32>) -> Option<&'static str> {
|
|
55
|
+
match raw {
|
|
56
|
+
Some(1314) => Some("EPERM"),
|
|
57
|
+
_ => None,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn code_for_kind(kind: std::io::ErrorKind) -> &'static str {
|
|
62
|
+
use std::io::ErrorKind;
|
|
63
|
+
match kind {
|
|
64
|
+
ErrorKind::NotFound => "ENOENT",
|
|
65
|
+
ErrorKind::PermissionDenied => "EACCES",
|
|
66
|
+
ErrorKind::AlreadyExists => "EEXIST",
|
|
67
|
+
ErrorKind::InvalidInput => "EINVAL",
|
|
68
|
+
ErrorKind::InvalidData => "EINVAL",
|
|
69
|
+
ErrorKind::WriteZero => "ENOSPC",
|
|
70
|
+
ErrorKind::StorageFull => "ENOSPC",
|
|
71
|
+
ErrorKind::ReadOnlyFilesystem => "EROFS",
|
|
72
|
+
ErrorKind::DirectoryNotEmpty => "ENOTEMPTY",
|
|
73
|
+
ErrorKind::IsADirectory => "EISDIR",
|
|
74
|
+
ErrorKind::NotADirectory => "ENOTDIR",
|
|
75
|
+
_ => "EIO",
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// The human half of a Node error message, by code.
|
|
80
|
+
///
|
|
81
|
+
/// Node's own table, because the tests compare against messages and because these
|
|
82
|
+
/// strings are what a developer recognises when a path is wrong.
|
|
83
|
+
fn description_for(code: &str) -> &'static str {
|
|
84
|
+
match code {
|
|
85
|
+
"ENOENT" => "no such file or directory",
|
|
86
|
+
"EACCES" => "permission denied",
|
|
87
|
+
"EPERM" => "operation not permitted",
|
|
88
|
+
"EEXIST" => "file already exists",
|
|
89
|
+
"ENOTDIR" => "not a directory",
|
|
90
|
+
"EISDIR" => "illegal operation on a directory",
|
|
91
|
+
"ENOTEMPTY" => "directory not empty",
|
|
92
|
+
"EINVAL" => "invalid argument",
|
|
93
|
+
"ELOOP" => "too many symbolic links encountered",
|
|
94
|
+
"ENOSPC" => "no space left on device",
|
|
95
|
+
"EROFS" => "read-only file system",
|
|
96
|
+
"EMFILE" => "too many open files",
|
|
97
|
+
"ENAMETOOLONG" => "name too long",
|
|
98
|
+
_ => "i/o error",
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// An `std::io::Error` as the `ModuleError` JS should receive.
|
|
103
|
+
///
|
|
104
|
+
/// `syscall` and `path` are attached by the JS layer, which knows the names the
|
|
105
|
+
/// caller used; the native side would have to guess between `open`, `lstat` and
|
|
106
|
+
/// the rest for what is already a single `stat` call.
|
|
107
|
+
pub fn from_io(error: std::io::Error, path: &str) -> ModuleError {
|
|
108
|
+
let code = code_for(&error);
|
|
109
|
+
ModuleError::with_code(code, format!("{code}: {}, {path}", description_for(code)))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// A failure that has no `std::io::Error`, such as a closed file descriptor.
|
|
113
|
+
pub fn invalid(code: &str, message: impl Into<String>) -> ModuleError {
|
|
114
|
+
ModuleError::with_code(code, message)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#[cfg(test)]
|
|
118
|
+
mod tests {
|
|
119
|
+
use super::*;
|
|
120
|
+
|
|
121
|
+
#[test]
|
|
122
|
+
fn a_missing_file_is_enoent_with_nodes_message() {
|
|
123
|
+
let error = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
|
|
124
|
+
let mapped = from_io(error, "/nope");
|
|
125
|
+
assert_eq!(mapped.code.as_deref(), Some("ENOENT"));
|
|
126
|
+
assert_eq!(mapped.message, "ENOENT: no such file or directory, /nope");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#[test]
|
|
130
|
+
fn a_denied_file_is_eacces() {
|
|
131
|
+
let error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "nope");
|
|
132
|
+
assert_eq!(from_io(error, "/x").code.as_deref(), Some("EACCES"));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#[cfg(windows)]
|
|
136
|
+
#[test]
|
|
137
|
+
fn a_missing_symlink_privilege_is_eperm_not_eio() {
|
|
138
|
+
// ERROR_PRIVILEGE_NOT_HELD: what `symlink_dir` returns without developer
|
|
139
|
+
// mode, and what Node reports as EPERM.
|
|
140
|
+
let error = std::io::Error::from_raw_os_error(1314);
|
|
141
|
+
assert_eq!(from_io(error, "C:\\link").code.as_deref(), Some("EPERM"));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#[test]
|
|
145
|
+
fn an_unknown_failure_still_has_a_code() {
|
|
146
|
+
let error = std::io::Error::new(std::io::ErrorKind::Other, "odd");
|
|
147
|
+
assert_eq!(from_io(error, "/x").code.as_deref(), Some("EIO"));
|
|
148
|
+
}
|
|
149
|
+
}
|