@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,640 @@
|
|
|
1
|
+
//! Reading, writing, copying, truncating, permissions and times.
|
|
2
|
+
//!
|
|
3
|
+
//! The async forms are thin wrappers over the sync ones: `std::fs` is blocking
|
|
4
|
+
//! either way, and this module has a thread of its own, so the only difference
|
|
5
|
+
//! is which thread the bridge calls the function on.
|
|
6
|
+
//!
|
|
7
|
+
//! Every failure goes through `errors::from_io`, so JS receives a Node code and
|
|
8
|
+
//! Node's message rather than an `std::io::Error` kind.
|
|
9
|
+
|
|
10
|
+
use std::fs;
|
|
11
|
+
use std::io::Write as _;
|
|
12
|
+
use std::path::Path;
|
|
13
|
+
|
|
14
|
+
use rn_gpui_module::{ArrayBuffer, ModuleError};
|
|
15
|
+
|
|
16
|
+
use crate::errors;
|
|
17
|
+
use crate::module::{kind, millis, open_options, system_time};
|
|
18
|
+
use crate::StatResult;
|
|
19
|
+
|
|
20
|
+
pub async fn read_file(path: String) -> Result<ArrayBuffer, ModuleError> {
|
|
21
|
+
read_file_sync(path)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pub async fn read_text_file(path: String) -> Result<String, ModuleError> {
|
|
25
|
+
read_text_file_sync(path)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pub async fn write_file(
|
|
29
|
+
path: String,
|
|
30
|
+
data: ArrayBuffer,
|
|
31
|
+
flags: f64,
|
|
32
|
+
mode: f64,
|
|
33
|
+
) -> Result<(), ModuleError> {
|
|
34
|
+
write_bytes(&path, data.as_slice(), flags, mode)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub async fn write_text_file(
|
|
38
|
+
path: String,
|
|
39
|
+
data: String,
|
|
40
|
+
flags: f64,
|
|
41
|
+
mode: f64,
|
|
42
|
+
) -> Result<(), ModuleError> {
|
|
43
|
+
write_bytes(&path, data.as_bytes(), flags, mode)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pub async fn stat(path: String, follow_symlinks: bool) -> Result<StatResult, ModuleError> {
|
|
47
|
+
stat_of(&path, follow_symlinks)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pub async fn rename(from: String, to: String) -> Result<(), ModuleError> {
|
|
51
|
+
fs::rename(&from, &to).map_err(|error| errors::from_io(error, &from))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
pub async fn copy(from: String, to: String, recursive: bool) -> Result<(), ModuleError> {
|
|
55
|
+
copy_path(&from, &to, recursive)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pub async fn truncate(path: String, length: f64) -> Result<(), ModuleError> {
|
|
59
|
+
let length = crate::module::length(length)?;
|
|
60
|
+
let file = fs::OpenOptions::new()
|
|
61
|
+
.write(true)
|
|
62
|
+
.open(&path)
|
|
63
|
+
.map_err(|error| errors::from_io(error, &path))?;
|
|
64
|
+
file.set_len(length as u64)
|
|
65
|
+
.map_err(|error| errors::from_io(error, &path))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
pub async fn utimes(path: String, atime_ms: f64, mtime_ms: f64) -> Result<(), ModuleError> {
|
|
69
|
+
let atime = filetime::FileTime::from_system_time(system_time(atime_ms));
|
|
70
|
+
let mtime = filetime::FileTime::from_system_time(system_time(mtime_ms));
|
|
71
|
+
filetime::set_file_times(&path, atime, mtime).map_err(|error| errors::from_io(error, &path))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
pub async fn chmod(path: String, mode: f64) -> Result<(), ModuleError> {
|
|
75
|
+
set_mode(&path, mode)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
pub fn read_file_sync(path: String) -> Result<ArrayBuffer, ModuleError> {
|
|
79
|
+
fs::read(&path)
|
|
80
|
+
.map(ArrayBuffer::from)
|
|
81
|
+
.map_err(|error| errors::from_io(error, &path))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pub fn read_text_file_sync(path: String) -> Result<String, ModuleError> {
|
|
85
|
+
let bytes = fs::read(&path).map_err(|error| errors::from_io(error, &path))?;
|
|
86
|
+
// Node decodes with replacement characters instead of rejecting a file that
|
|
87
|
+
// is not valid UTF-8, so reading a mis-encoded file still yields text.
|
|
88
|
+
Ok(String::from_utf8_lossy(&bytes).into_owned())
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pub fn write_file_sync(
|
|
92
|
+
path: String,
|
|
93
|
+
data: ArrayBuffer,
|
|
94
|
+
flags: f64,
|
|
95
|
+
mode: f64,
|
|
96
|
+
) -> Result<bool, ModuleError> {
|
|
97
|
+
write_bytes(&path, data.as_slice(), flags, mode)?;
|
|
98
|
+
Ok(true)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
pub fn write_text_file_sync(
|
|
102
|
+
path: String,
|
|
103
|
+
data: String,
|
|
104
|
+
flags: f64,
|
|
105
|
+
mode: f64,
|
|
106
|
+
) -> Result<bool, ModuleError> {
|
|
107
|
+
write_bytes(&path, data.as_bytes(), flags, mode)?;
|
|
108
|
+
Ok(true)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// `Ok(None)` for a path that is not there: that is what `existsSync` asks, and
|
|
112
|
+
/// only a missing path means "no". A permission failure is still an error.
|
|
113
|
+
pub fn stat_sync(path: String, follow_symlinks: bool) -> Result<Option<StatResult>, ModuleError> {
|
|
114
|
+
match stat_of(&path, follow_symlinks) {
|
|
115
|
+
Ok(result) => Ok(Some(result)),
|
|
116
|
+
Err(error) if error.code.as_deref() == Some("ENOENT") => Ok(None),
|
|
117
|
+
Err(error) => Err(error),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
pub fn exists_sync(path: String) -> Result<bool, ModuleError> {
|
|
122
|
+
Ok(matches!(stat_sync(path, true), Ok(Some(_))))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
pub fn rename_sync(from: String, to: String) -> Result<bool, ModuleError> {
|
|
126
|
+
fs::rename(&from, &to).map_err(|error| errors::from_io(error, &from))?;
|
|
127
|
+
Ok(true)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
pub fn copy_file_sync(from: String, to: String) -> Result<bool, ModuleError> {
|
|
131
|
+
fs::copy(&from, &to).map_err(|error| errors::from_io(error, &from))?;
|
|
132
|
+
Ok(true)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// The one place a write happens, so bytes and text share the flag and mode
|
|
136
|
+
/// translation exactly.
|
|
137
|
+
fn write_bytes(path: &str, data: &[u8], flags: f64, mode: f64) -> Result<(), ModuleError> {
|
|
138
|
+
let mut file = open_options(flags, crate::module::mode(mode))
|
|
139
|
+
.open(path)
|
|
140
|
+
.map_err(|error| errors::from_io(error, path))?;
|
|
141
|
+
file.write_all(data)
|
|
142
|
+
.map_err(|error| errors::from_io(error, path))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fn stat_of(path: &str, follow_symlinks: bool) -> Result<StatResult, ModuleError> {
|
|
146
|
+
let metadata = if follow_symlinks {
|
|
147
|
+
fs::metadata(path)
|
|
148
|
+
} else {
|
|
149
|
+
fs::symlink_metadata(path)
|
|
150
|
+
}
|
|
151
|
+
.map_err(|error| errors::from_io(error, path))?;
|
|
152
|
+
Ok(stat_result(&metadata))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// The generated `StatResult`, filled from `std::fs::Metadata`.
|
|
156
|
+
///
|
|
157
|
+
/// Node's `Stats` names the Unix inode fields even on Windows, where they have
|
|
158
|
+
/// no meaning; zero is what Node itself reports there.
|
|
159
|
+
fn stat_result(metadata: &fs::Metadata) -> StatResult {
|
|
160
|
+
let file_type = metadata.file_type();
|
|
161
|
+
let kind = if file_type.is_file() {
|
|
162
|
+
kind::FILE
|
|
163
|
+
} else if file_type.is_dir() {
|
|
164
|
+
kind::DIRECTORY
|
|
165
|
+
} else if file_type.is_symlink() {
|
|
166
|
+
kind::SYMLINK
|
|
167
|
+
} else {
|
|
168
|
+
kind::OTHER
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
#[cfg(unix)]
|
|
172
|
+
let (mode, dev, ino, nlink, uid, gid) = {
|
|
173
|
+
use std::os::unix::fs::MetadataExt as _;
|
|
174
|
+
(
|
|
175
|
+
metadata.mode() as f64,
|
|
176
|
+
metadata.dev() as f64,
|
|
177
|
+
metadata.ino() as f64,
|
|
178
|
+
metadata.nlink() as f64,
|
|
179
|
+
metadata.uid() as f64,
|
|
180
|
+
metadata.gid() as f64,
|
|
181
|
+
)
|
|
182
|
+
};
|
|
183
|
+
#[cfg(not(unix))]
|
|
184
|
+
let (mode, dev, ino, nlink, uid, gid) = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
|
|
185
|
+
|
|
186
|
+
// `ctime` is the inode change time on Unix; Windows has no such thing, and
|
|
187
|
+
// its creation time is the closest equivalent.
|
|
188
|
+
#[cfg(unix)]
|
|
189
|
+
let ctime_ms = {
|
|
190
|
+
use std::os::unix::fs::MetadataExt as _;
|
|
191
|
+
metadata.ctime() as f64 * 1000.0 + metadata.ctime_nsec() as f64 / 1_000_000.0
|
|
192
|
+
};
|
|
193
|
+
#[cfg(not(unix))]
|
|
194
|
+
let ctime_ms = metadata
|
|
195
|
+
.created()
|
|
196
|
+
.map(millis)
|
|
197
|
+
.unwrap_or_else(|_| metadata.modified().map(millis).unwrap_or(0.0));
|
|
198
|
+
|
|
199
|
+
// A file system that cannot report creation gives 0, as Node does, rather
|
|
200
|
+
// than pretending the modification time is the birth time.
|
|
201
|
+
let birthtime_ms = metadata.created().map(millis).unwrap_or(0.0);
|
|
202
|
+
|
|
203
|
+
StatResult {
|
|
204
|
+
size: metadata.len() as f64,
|
|
205
|
+
mode,
|
|
206
|
+
kind,
|
|
207
|
+
atime_ms: metadata.accessed().map(millis).unwrap_or(0.0),
|
|
208
|
+
mtime_ms: metadata.modified().map(millis).unwrap_or(0.0),
|
|
209
|
+
ctime_ms,
|
|
210
|
+
birthtime_ms,
|
|
211
|
+
dev,
|
|
212
|
+
ino,
|
|
213
|
+
nlink,
|
|
214
|
+
uid,
|
|
215
|
+
gid,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
fn copy_path(from: &str, to: &str, recursive: bool) -> Result<(), ModuleError> {
|
|
220
|
+
let metadata = fs::symlink_metadata(from).map_err(|error| errors::from_io(error, from))?;
|
|
221
|
+
if !metadata.is_dir() {
|
|
222
|
+
fs::copy(from, to).map_err(|error| errors::from_io(error, from))?;
|
|
223
|
+
return Ok(());
|
|
224
|
+
}
|
|
225
|
+
if !recursive {
|
|
226
|
+
return Err(errors::invalid(
|
|
227
|
+
"EISDIR",
|
|
228
|
+
format!("EISDIR: illegal operation on a directory, {from}"),
|
|
229
|
+
));
|
|
230
|
+
}
|
|
231
|
+
copy_tree(Path::new(from), Path::new(to))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/// Refuse to copy a directory into itself or a directory beneath it.
|
|
235
|
+
///
|
|
236
|
+
/// The destination may not exist yet, so the comparison is against its nearest
|
|
237
|
+
/// existing ancestor: `a` into `a/b/c` has to be caught before `a/b/c` exists,
|
|
238
|
+
/// which is the whole point. Both sides are canonicalised so a symlinked path
|
|
239
|
+
/// cannot slip past the check.
|
|
240
|
+
///
|
|
241
|
+
/// Node's code and wording are reproduced rather than invented; `cpSync` reports
|
|
242
|
+
/// `ERR_FS_CP_EINVAL` with `Cannot copy /tmp/cpa/ to a subdirectory of self
|
|
243
|
+
/// /tmp/cpa/sub`, and callers match on both.
|
|
244
|
+
fn refuse_copy_into_self(from: &Path, to: &Path) -> Result<(), ModuleError> {
|
|
245
|
+
let resolve = |path: &Path| -> Option<std::path::PathBuf> {
|
|
246
|
+
let mut existing = path.to_path_buf();
|
|
247
|
+
let mut tail = Vec::new();
|
|
248
|
+
loop {
|
|
249
|
+
if let Ok(canonical) = existing.canonicalize() {
|
|
250
|
+
return Some(
|
|
251
|
+
tail.iter()
|
|
252
|
+
.rev()
|
|
253
|
+
.fold(canonical, |base, part| base.join(part)),
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
let name = existing.file_name().map(|name| name.to_owned())?;
|
|
257
|
+
tail.push(name);
|
|
258
|
+
if !existing.pop() {
|
|
259
|
+
return None;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
let Some(source) = resolve(from) else {
|
|
264
|
+
return Ok(());
|
|
265
|
+
};
|
|
266
|
+
let Some(destination) = resolve(to) else {
|
|
267
|
+
return Ok(());
|
|
268
|
+
};
|
|
269
|
+
if destination == source || destination.starts_with(&source) {
|
|
270
|
+
return Err(errors::invalid(
|
|
271
|
+
"ERR_FS_CP_EINVAL",
|
|
272
|
+
format!(
|
|
273
|
+
"Cannot copy {} to a subdirectory of self {}",
|
|
274
|
+
from.display(),
|
|
275
|
+
to.display()
|
|
276
|
+
),
|
|
277
|
+
));
|
|
278
|
+
}
|
|
279
|
+
Ok(())
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
fn copy_tree(from: &Path, to: &Path) -> Result<(), ModuleError> {
|
|
283
|
+
let target = to.to_string_lossy().into_owned();
|
|
284
|
+
// Checked before anything is created: copying a directory into its own
|
|
285
|
+
// subtree would otherwise recurse forever, and the tree it built on the way
|
|
286
|
+
// down would have to be cleaned up by the caller.
|
|
287
|
+
refuse_copy_into_self(from, to)?;
|
|
288
|
+
fs::create_dir_all(to).map_err(|error| errors::from_io(error, &target))?;
|
|
289
|
+
let children = fs::read_dir(from).map_err(|error| errors::from_io(error, &source(from)))?;
|
|
290
|
+
for child in children {
|
|
291
|
+
let child = child.map_err(|error| errors::from_io(error, &source(from)))?;
|
|
292
|
+
let path = child.path();
|
|
293
|
+
let destination = to.join(child.file_name());
|
|
294
|
+
// `file_type` does not follow links, which keeps a link cycle in the
|
|
295
|
+
// source tree from turning into infinite recursion.
|
|
296
|
+
let file_type = child
|
|
297
|
+
.file_type()
|
|
298
|
+
.map_err(|error| errors::from_io(error, &source(&path)))?;
|
|
299
|
+
if file_type.is_dir() {
|
|
300
|
+
copy_tree(&path, &destination)?;
|
|
301
|
+
} else if file_type.is_symlink() {
|
|
302
|
+
copy_symlink(&path, &destination)?;
|
|
303
|
+
} else {
|
|
304
|
+
fs::copy(&path, &destination)
|
|
305
|
+
.map_err(|error| errors::from_io(error, &source(&path)))?;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
Ok(())
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
#[cfg(unix)]
|
|
312
|
+
fn copy_symlink(from: &Path, to: &Path) -> Result<(), ModuleError> {
|
|
313
|
+
let target = fs::read_link(from).map_err(|error| errors::from_io(error, &source(from)))?;
|
|
314
|
+
std::os::unix::fs::symlink(&target, to).map_err(|error| errors::from_io(error, &source(from)))
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/// Windows needs a privilege to create a symbolic link, so there the link is
|
|
318
|
+
/// followed and its target copied, which is what a caller copying a tree wants
|
|
319
|
+
/// to end up with anyway.
|
|
320
|
+
#[cfg(not(unix))]
|
|
321
|
+
fn copy_symlink(from: &Path, to: &Path) -> Result<(), ModuleError> {
|
|
322
|
+
fs::copy(from, to)
|
|
323
|
+
.map(|_| ())
|
|
324
|
+
.map_err(|error| errors::from_io(error, &source(from)))
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
fn source(path: &Path) -> String {
|
|
328
|
+
path.to_string_lossy().into_owned()
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
#[cfg(unix)]
|
|
332
|
+
fn set_mode(path: &str, mode: f64) -> Result<(), ModuleError> {
|
|
333
|
+
use std::os::unix::fs::PermissionsExt as _;
|
|
334
|
+
let bits = if mode.is_finite() && mode >= 0.0 {
|
|
335
|
+
mode as u32 & 0o7777
|
|
336
|
+
} else {
|
|
337
|
+
0o666
|
|
338
|
+
};
|
|
339
|
+
fs::set_permissions(path, fs::Permissions::from_mode(bits))
|
|
340
|
+
.map_err(|error| errors::from_io(error, path))
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/// Windows has no permission bits. The closest equivalent is the read-only
|
|
344
|
+
/// attribute, so a mode without a write bit marks the file read-only and any
|
|
345
|
+
/// other mode clears it; nothing else about the mode survives.
|
|
346
|
+
#[cfg(not(unix))]
|
|
347
|
+
fn set_mode(path: &str, mode: f64) -> Result<(), ModuleError> {
|
|
348
|
+
let metadata = fs::metadata(path).map_err(|error| errors::from_io(error, path))?;
|
|
349
|
+
let mut permissions = metadata.permissions();
|
|
350
|
+
permissions.set_readonly(mode.is_finite() && mode > 0.0 && mode as u32 & 0o200 == 0);
|
|
351
|
+
fs::set_permissions(path, permissions).map_err(|error| errors::from_io(error, path))
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#[cfg(test)]
|
|
355
|
+
mod tests {
|
|
356
|
+
use super::*;
|
|
357
|
+
use crate::module::flags;
|
|
358
|
+
use tempfile::TempDir;
|
|
359
|
+
|
|
360
|
+
/// The async wrappers never actually suspend (they call `std::fs` directly),
|
|
361
|
+
/// so a single-poll executor is enough and keeps the tests free of a runtime.
|
|
362
|
+
fn block_on<F: std::future::Future>(future: F) -> F::Output {
|
|
363
|
+
let mut future = std::pin::pin!(future);
|
|
364
|
+
let waker = std::task::Waker::noop();
|
|
365
|
+
let mut context = std::task::Context::from_waker(waker);
|
|
366
|
+
// One poll, no loop: these helpers do their work inline, so a `Pending`
|
|
367
|
+
// would mean the helper is not synchronous after all, which the test
|
|
368
|
+
// should fail loudly on rather than spin.
|
|
369
|
+
match future.as_mut().poll(&mut context) {
|
|
370
|
+
std::task::Poll::Ready(value) => value,
|
|
371
|
+
std::task::Poll::Pending => panic!("the wrappers are synchronous"),
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
fn tempdir() -> TempDir {
|
|
376
|
+
TempDir::new().expect("a temporary directory")
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
fn text(path: &Path) -> String {
|
|
380
|
+
path.to_string_lossy().into_owned()
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/// `'w'` in Node's table.
|
|
384
|
+
fn write_flags() -> f64 {
|
|
385
|
+
f64::from(flags::WRONLY | flags::CREAT | flags::TRUNC)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
#[test]
|
|
389
|
+
fn text_round_trip() {
|
|
390
|
+
let dir = tempdir();
|
|
391
|
+
let path = dir.path().join("note.txt");
|
|
392
|
+
|
|
393
|
+
block_on(write_text_file(
|
|
394
|
+
text(&path),
|
|
395
|
+
"hello".to_string(),
|
|
396
|
+
write_flags(),
|
|
397
|
+
0.0,
|
|
398
|
+
))
|
|
399
|
+
.expect("write");
|
|
400
|
+
assert_eq!(
|
|
401
|
+
block_on(read_text_file(text(&path))).expect("read"),
|
|
402
|
+
"hello"
|
|
403
|
+
);
|
|
404
|
+
assert_eq!(
|
|
405
|
+
read_text_file_sync(text(&path)).expect("read sync"),
|
|
406
|
+
"hello"
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
assert!(
|
|
410
|
+
write_text_file_sync(text(&path), "again".to_string(), write_flags(), 0.0)
|
|
411
|
+
.expect("write sync")
|
|
412
|
+
);
|
|
413
|
+
assert_eq!(fs::read_to_string(&path).expect("on disk"), "again");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
#[test]
|
|
417
|
+
fn bytes_round_trip() {
|
|
418
|
+
let dir = tempdir();
|
|
419
|
+
let path = dir.path().join("bytes.bin");
|
|
420
|
+
let bytes = ArrayBuffer::from(vec![0u8, 1, 254, 255]);
|
|
421
|
+
|
|
422
|
+
block_on(write_file(text(&path), bytes.clone(), write_flags(), 0.0)).expect("write");
|
|
423
|
+
assert_eq!(
|
|
424
|
+
block_on(read_file(text(&path))).expect("read").as_slice(),
|
|
425
|
+
&[0u8, 1, 254, 255]
|
|
426
|
+
);
|
|
427
|
+
assert_eq!(
|
|
428
|
+
read_file_sync(text(&path)).expect("read sync").into_vec(),
|
|
429
|
+
vec![0u8, 1, 254, 255]
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
#[test]
|
|
434
|
+
fn an_append_flag_keeps_the_existing_bytes() {
|
|
435
|
+
let dir = tempdir();
|
|
436
|
+
let path = dir.path().join("log.txt");
|
|
437
|
+
fs::write(&path, b"one").expect("seed");
|
|
438
|
+
|
|
439
|
+
block_on(write_text_file(
|
|
440
|
+
text(&path),
|
|
441
|
+
"two".to_string(),
|
|
442
|
+
f64::from(flags::WRONLY | flags::CREAT | flags::APPEND),
|
|
443
|
+
0.0,
|
|
444
|
+
))
|
|
445
|
+
.expect("append");
|
|
446
|
+
|
|
447
|
+
assert_eq!(fs::read_to_string(&path).expect("on disk"), "onetwo");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
#[test]
|
|
451
|
+
fn stat_reports_kind_and_size() {
|
|
452
|
+
let dir = tempdir();
|
|
453
|
+
let file = dir.path().join("data.bin");
|
|
454
|
+
fs::write(&file, b"12345").expect("seed");
|
|
455
|
+
|
|
456
|
+
let stats = block_on(stat(text(&file), true)).expect("stat");
|
|
457
|
+
assert_eq!(stats.kind, kind::FILE);
|
|
458
|
+
assert_eq!(stats.size, 5.0);
|
|
459
|
+
assert!(stats.mtime_ms > 0.0);
|
|
460
|
+
#[cfg(unix)]
|
|
461
|
+
assert_ne!(
|
|
462
|
+
stats.mode as u32 & 0o777,
|
|
463
|
+
0,
|
|
464
|
+
"the mode bits come from MetadataExt"
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
let directory = block_on(stat(text(dir.path()), true)).expect("stat dir");
|
|
468
|
+
assert_eq!(directory.kind, kind::DIRECTORY);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
#[cfg(unix)]
|
|
472
|
+
#[test]
|
|
473
|
+
fn stat_follows_links_only_when_asked() {
|
|
474
|
+
let dir = tempdir();
|
|
475
|
+
let target = dir.path().join("target.txt");
|
|
476
|
+
fs::write(&target, b"x").expect("seed");
|
|
477
|
+
let link = dir.path().join("link.txt");
|
|
478
|
+
std::os::unix::fs::symlink(&target, &link).expect("symlink");
|
|
479
|
+
|
|
480
|
+
assert_eq!(
|
|
481
|
+
block_on(stat(text(&link), false)).expect("lstat").kind,
|
|
482
|
+
kind::SYMLINK
|
|
483
|
+
);
|
|
484
|
+
assert_eq!(
|
|
485
|
+
block_on(stat(text(&link), true)).expect("stat").kind,
|
|
486
|
+
kind::FILE
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
#[test]
|
|
491
|
+
fn stat_sync_is_none_for_a_missing_path() {
|
|
492
|
+
let dir = tempdir();
|
|
493
|
+
let missing = text(&dir.path().join("absent.txt"));
|
|
494
|
+
assert!(stat_sync(missing, true).expect("stat").is_none());
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
#[test]
|
|
498
|
+
fn exists_sync_is_false_for_a_missing_path() {
|
|
499
|
+
let dir = tempdir();
|
|
500
|
+
assert!(!exists_sync(text(&dir.path().join("absent.txt"))).expect("exists"));
|
|
501
|
+
assert!(exists_sync(text(dir.path())).expect("exists"));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
#[test]
|
|
505
|
+
fn a_missing_file_is_enoent() {
|
|
506
|
+
let dir = tempdir();
|
|
507
|
+
let missing = text(&dir.path().join("gone.txt"));
|
|
508
|
+
|
|
509
|
+
let error = block_on(read_text_file(missing.clone())).expect_err("read");
|
|
510
|
+
assert_eq!(error.code.as_deref(), Some("ENOENT"));
|
|
511
|
+
assert!(error
|
|
512
|
+
.message
|
|
513
|
+
.starts_with("ENOENT: no such file or directory"));
|
|
514
|
+
|
|
515
|
+
let error = read_file_sync(missing).expect_err("read sync");
|
|
516
|
+
assert_eq!(error.code.as_deref(), Some("ENOENT"));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
#[test]
|
|
520
|
+
fn rename_moves_a_file() {
|
|
521
|
+
let dir = tempdir();
|
|
522
|
+
let from = dir.path().join("a.txt");
|
|
523
|
+
let to = dir.path().join("b.txt");
|
|
524
|
+
fs::write(&from, b"a").expect("seed");
|
|
525
|
+
|
|
526
|
+
block_on(rename(text(&from), text(&to))).expect("rename");
|
|
527
|
+
assert!(!from.exists());
|
|
528
|
+
assert_eq!(fs::read_to_string(&to).expect("on disk"), "a");
|
|
529
|
+
|
|
530
|
+
assert!(rename_sync(text(&to), text(&from)).expect("rename sync"));
|
|
531
|
+
assert!(from.exists());
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
#[test]
|
|
535
|
+
fn copy_copies_a_file() {
|
|
536
|
+
let dir = tempdir();
|
|
537
|
+
let from = dir.path().join("one.txt");
|
|
538
|
+
fs::write(&from, b"copy me").expect("seed");
|
|
539
|
+
let to = dir.path().join("two.txt");
|
|
540
|
+
|
|
541
|
+
block_on(copy(text(&from), text(&to), false)).expect("copy");
|
|
542
|
+
assert_eq!(fs::read_to_string(&to).expect("on disk"), "copy me");
|
|
543
|
+
|
|
544
|
+
let third = dir.path().join("three.txt");
|
|
545
|
+
assert!(copy_file_sync(text(&from), text(&third)).expect("copy sync"));
|
|
546
|
+
assert_eq!(fs::read_to_string(&third).expect("on disk"), "copy me");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
#[test]
|
|
550
|
+
fn copy_copies_a_directory_tree_when_recursive() {
|
|
551
|
+
let dir = tempdir();
|
|
552
|
+
let from = dir.path().join("tree");
|
|
553
|
+
fs::create_dir_all(from.join("nested")).expect("tree");
|
|
554
|
+
fs::write(from.join("top.txt"), b"top").expect("seed");
|
|
555
|
+
fs::write(from.join("nested").join("deep.txt"), b"deep").expect("seed");
|
|
556
|
+
let to = dir.path().join("copy");
|
|
557
|
+
|
|
558
|
+
block_on(copy(text(&from), text(&to), true)).expect("copy tree");
|
|
559
|
+
assert_eq!(
|
|
560
|
+
fs::read_to_string(to.join("nested").join("deep.txt")).expect("on disk"),
|
|
561
|
+
"deep"
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
let error = block_on(copy(text(&from), text(&dir.path().join("nope")), false))
|
|
565
|
+
.expect_err("a directory without recursive");
|
|
566
|
+
assert_eq!(error.code.as_deref(), Some("EISDIR"));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
#[test]
|
|
570
|
+
fn copying_a_directory_into_itself_is_refused() {
|
|
571
|
+
let dir = tempdir();
|
|
572
|
+
let from = dir.path().join("tree");
|
|
573
|
+
fs::create_dir_all(from.join("sub")).expect("tree");
|
|
574
|
+
fs::write(from.join("top.txt"), b"top").expect("seed");
|
|
575
|
+
|
|
576
|
+
// Into a directory beneath itself. This used to recurse until the path
|
|
577
|
+
// got too long, and left whatever it had built on the way down.
|
|
578
|
+
let into_self = from.join("sub");
|
|
579
|
+
let error = block_on(copy(text(&from), text(&into_self), true))
|
|
580
|
+
.expect_err("copy into a subdirectory of self");
|
|
581
|
+
assert_eq!(error.code.as_deref(), Some("ERR_FS_CP_EINVAL"));
|
|
582
|
+
assert!(
|
|
583
|
+
error.message.starts_with("Cannot copy"),
|
|
584
|
+
"{}",
|
|
585
|
+
error.message
|
|
586
|
+
);
|
|
587
|
+
// The check runs before anything is created, so the destination is still
|
|
588
|
+
// just the empty directory it was.
|
|
589
|
+
assert_eq!(
|
|
590
|
+
fs::read_dir(&into_self).expect("still there").count(),
|
|
591
|
+
0,
|
|
592
|
+
"nothing may be created when the copy is refused"
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
// The directory itself is the same case.
|
|
596
|
+
let error = block_on(copy(text(&from), text(&from), true)).expect_err("copy onto itself");
|
|
597
|
+
assert_eq!(error.code.as_deref(), Some("ERR_FS_CP_EINVAL"));
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
#[test]
|
|
601
|
+
fn truncate_changes_the_length() {
|
|
602
|
+
let dir = tempdir();
|
|
603
|
+
let path = dir.path().join("long.txt");
|
|
604
|
+
fs::write(&path, b"0123456789").expect("seed");
|
|
605
|
+
|
|
606
|
+
block_on(truncate(text(&path), 4.0)).expect("truncate");
|
|
607
|
+
assert_eq!(fs::read_to_string(&path).expect("on disk"), "0123");
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
#[test]
|
|
611
|
+
fn utimes_sets_both_times() {
|
|
612
|
+
let dir = tempdir();
|
|
613
|
+
let path = dir.path().join("stamp.txt");
|
|
614
|
+
fs::write(&path, b"x").expect("seed");
|
|
615
|
+
|
|
616
|
+
block_on(utimes(
|
|
617
|
+
text(&path),
|
|
618
|
+
1_000_000_000_000.0,
|
|
619
|
+
1_100_000_000_000.0,
|
|
620
|
+
))
|
|
621
|
+
.expect("utimes");
|
|
622
|
+
|
|
623
|
+
let stats = block_on(stat(text(&path), true)).expect("stat");
|
|
624
|
+
assert_eq!(stats.mtime_ms, 1_100_000_000_000.0);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
#[cfg(unix)]
|
|
628
|
+
#[test]
|
|
629
|
+
fn chmod_sets_the_permission_bits() {
|
|
630
|
+
use std::os::unix::fs::PermissionsExt as _;
|
|
631
|
+
let dir = tempdir();
|
|
632
|
+
let path = dir.path().join("locked.txt");
|
|
633
|
+
fs::write(&path, b"x").expect("seed");
|
|
634
|
+
|
|
635
|
+
block_on(chmod(text(&path), f64::from(0o640u32))).expect("chmod");
|
|
636
|
+
|
|
637
|
+
let mode = fs::metadata(&path).expect("metadata").permissions().mode() & 0o777;
|
|
638
|
+
assert_eq!(mode, 0o640);
|
|
639
|
+
}
|
|
640
|
+
}
|