@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,697 @@
|
|
|
1
|
+
//! Open descriptors: `open`, `read`, `write`, `fstat`, `fsync`, `ftruncate`
|
|
2
|
+
//! and `close`.
|
|
3
|
+
//!
|
|
4
|
+
//! A descriptor is an id from the module's table plus a clone of the file that
|
|
5
|
+
//! shares its offset, which is what a descriptor is to the OS. The table owns
|
|
6
|
+
//! the original, so dropping the module closes every handle; ids are never
|
|
7
|
+
//! reused, so a stale `fd` that JS kept across a `close` fails with `EBADF`
|
|
8
|
+
//! instead of reaching an unrelated file.
|
|
9
|
+
//!
|
|
10
|
+
//! These functions are synchronous even though the trait methods around them are
|
|
11
|
+
//! `async`: the module has a thread of its own, so a blocking call here never
|
|
12
|
+
//! blocks JS or the UI.
|
|
13
|
+
|
|
14
|
+
use std::fs::File;
|
|
15
|
+
use std::io::{Read as _, Seek as _, Write as _};
|
|
16
|
+
|
|
17
|
+
use rn_gpui_module::{ArrayBuffer, ModuleError};
|
|
18
|
+
|
|
19
|
+
use crate::errors;
|
|
20
|
+
use crate::module::{bad_fd, descriptor, flags, kind, millis, open_options};
|
|
21
|
+
use crate::StatResult;
|
|
22
|
+
|
|
23
|
+
pub fn open(
|
|
24
|
+
module: &crate::RNFsModule,
|
|
25
|
+
path: String,
|
|
26
|
+
flags: f64,
|
|
27
|
+
mode: f64,
|
|
28
|
+
) -> Result<f64, ModuleError> {
|
|
29
|
+
// The table owns the file; everything the caller asks of it goes through
|
|
30
|
+
// `module.file(fd)`, which hands back a clone sharing the same offset.
|
|
31
|
+
let file = open_options(flags, crate::module::mode(mode))
|
|
32
|
+
.open(&path)
|
|
33
|
+
.map_err(|error| errors::from_io(error, &path))?;
|
|
34
|
+
|
|
35
|
+
let id = module.take_fd();
|
|
36
|
+
// `RDONLY` is zero, so "writable" is the presence of any write bit rather
|
|
37
|
+
// than the absence of a read bit.
|
|
38
|
+
let writable = (flags as i32) & (flags::WRONLY | flags::RDWR) != 0;
|
|
39
|
+
module
|
|
40
|
+
.files
|
|
41
|
+
.lock()
|
|
42
|
+
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
43
|
+
.insert(id, (file, writable));
|
|
44
|
+
Ok(f64::from(id))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pub fn read(
|
|
48
|
+
module: &crate::RNFsModule,
|
|
49
|
+
fd: f64,
|
|
50
|
+
length: f64,
|
|
51
|
+
position: f64,
|
|
52
|
+
) -> Result<ArrayBuffer, ModuleError> {
|
|
53
|
+
let id = descriptor(fd)?;
|
|
54
|
+
let length = crate::module::length(length)?;
|
|
55
|
+
let mut file = module.file(fd)?;
|
|
56
|
+
|
|
57
|
+
// Nothing larger than the file can be read, so the buffer is capped at what
|
|
58
|
+
// remains rather than at what was asked for. A caller that passes a huge
|
|
59
|
+
// `length` — which `readToEnd` used to do deliberately — would otherwise ask
|
|
60
|
+
// for that many bytes up front and get whatever the allocator did with it.
|
|
61
|
+
let cap = match offset(position)? {
|
|
62
|
+
Some(start) => file
|
|
63
|
+
.metadata()
|
|
64
|
+
.ok()
|
|
65
|
+
.and_then(|meta| meta.len().checked_sub(start))
|
|
66
|
+
.map_or(length, |remaining| length.min(remaining as usize)),
|
|
67
|
+
None => file
|
|
68
|
+
.metadata()
|
|
69
|
+
.ok()
|
|
70
|
+
.and_then(|meta| {
|
|
71
|
+
// A cursor read starts wherever the handle is, so the remainder
|
|
72
|
+
// is measured from there rather than from the start.
|
|
73
|
+
let current = file.stream_position().ok()?;
|
|
74
|
+
meta.len().checked_sub(current)
|
|
75
|
+
})
|
|
76
|
+
.map_or(length, |remaining| length.min(remaining as usize)),
|
|
77
|
+
};
|
|
78
|
+
let mut buffer = vec![0u8; cap];
|
|
79
|
+
let filled = match offset(position)? {
|
|
80
|
+
Some(start) => read_at(&mut file, &mut buffer, start, id)?,
|
|
81
|
+
None => read_cursor(&mut file, &mut buffer, id)?,
|
|
82
|
+
};
|
|
83
|
+
// A read that stopped at end of file is a short read, not a failure, so the
|
|
84
|
+
// buffer is shrunk to what the OS actually produced rather than padded.
|
|
85
|
+
buffer.truncate(filled);
|
|
86
|
+
Ok(ArrayBuffer::from(buffer))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pub fn write(
|
|
90
|
+
module: &crate::RNFsModule,
|
|
91
|
+
fd: f64,
|
|
92
|
+
data: ArrayBuffer,
|
|
93
|
+
position: f64,
|
|
94
|
+
) -> Result<f64, ModuleError> {
|
|
95
|
+
let id = descriptor(fd)?;
|
|
96
|
+
let (mut file, writable) = module.file_and_writability(fd)?;
|
|
97
|
+
// Checked from the flags the handle was opened with, not from what the OS
|
|
98
|
+
// says: a write to a read-only handle is `EBADF` in Node, `EACCES` on
|
|
99
|
+
// Windows and `EBADF` on Unix.
|
|
100
|
+
if !writable {
|
|
101
|
+
return Err(bad_fd(id));
|
|
102
|
+
}
|
|
103
|
+
let bytes = data.as_slice();
|
|
104
|
+
if bytes.is_empty() {
|
|
105
|
+
return Ok(0.0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Read before the borrow below: `positioned_write` needs `&mut File` so the
|
|
109
|
+
// Windows branch can seek, and a match guard borrowing the file would overlap.
|
|
110
|
+
let append_only = appends(&file);
|
|
111
|
+
let written = match offset(position)? {
|
|
112
|
+
// An append-mode descriptor appends whatever offset the caller passes:
|
|
113
|
+
// a cursor write does that because the file was opened with `O_APPEND`
|
|
114
|
+
// (or `FILE_APPEND_DATA`), and Linux also appends for a positioned
|
|
115
|
+
// write. Ignoring the offset keeps the platforms alike and is what
|
|
116
|
+
// callers of `appendFile` expect.
|
|
117
|
+
Some(start) if !append_only => write_at(&mut file, bytes, start, id)?,
|
|
118
|
+
_ => write_cursor(&mut file, bytes, id)?,
|
|
119
|
+
};
|
|
120
|
+
Ok(written as f64)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
pub fn fstat(module: &crate::RNFsModule, fd: f64) -> Result<StatResult, ModuleError> {
|
|
124
|
+
let id = descriptor(fd)?;
|
|
125
|
+
let file = module.file(fd)?;
|
|
126
|
+
let metadata = file.metadata().map_err(|error| io_error(error, id))?;
|
|
127
|
+
Ok(stat_result(&metadata))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
pub fn fsync(module: &crate::RNFsModule, fd: f64) -> Result<(), ModuleError> {
|
|
131
|
+
let id = descriptor(fd)?;
|
|
132
|
+
let file = module.file(fd)?;
|
|
133
|
+
file.sync_all().map_err(|error| io_error(error, id))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
pub fn ftruncate(module: &crate::RNFsModule, fd: f64, length: f64) -> Result<(), ModuleError> {
|
|
137
|
+
let id = descriptor(fd)?;
|
|
138
|
+
let length = crate::module::length(length)?;
|
|
139
|
+
let file = module.file(fd)?;
|
|
140
|
+
file.set_len(length as u64)
|
|
141
|
+
.map_err(|error| io_error(error, id))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
pub fn close(module: &crate::RNFsModule, fd: f64) -> Result<(), ModuleError> {
|
|
145
|
+
let id = descriptor(fd)?;
|
|
146
|
+
let mut files = module
|
|
147
|
+
.files
|
|
148
|
+
.lock()
|
|
149
|
+
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
150
|
+
// Removing rather than merely dropping the entry is what makes a second
|
|
151
|
+
// close report `EBADF`: the id is gone, and it is never handed out again.
|
|
152
|
+
files.remove(&id).ok_or_else(|| bad_fd(id))?;
|
|
153
|
+
Ok(())
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// A failure from an operation on a descriptor.
|
|
157
|
+
///
|
|
158
|
+
/// The table may still know an id the kernel has already rejected — a write to a
|
|
159
|
+
/// read-only descriptor is the usual way — and Node reports that as `EBADF`, not
|
|
160
|
+
/// as a generic I/O error. Everything else goes through the shared mapping, with
|
|
161
|
+
/// the descriptor standing in for the path a Node message names.
|
|
162
|
+
fn io_error(error: std::io::Error, id: u32) -> ModuleError {
|
|
163
|
+
#[cfg(unix)]
|
|
164
|
+
if error.raw_os_error() == Some(libc::EBADF) {
|
|
165
|
+
return bad_fd(id);
|
|
166
|
+
}
|
|
167
|
+
errors::from_io(error, &id.to_string())
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// The byte offset a `position` argument names, or `None` for "the current
|
|
171
|
+
/// offset", which is what a negative position means to `node:fs`.
|
|
172
|
+
fn offset(position: f64) -> Result<Option<u64>, ModuleError> {
|
|
173
|
+
if !position.is_finite() {
|
|
174
|
+
return Err(errors::invalid(
|
|
175
|
+
"EINVAL",
|
|
176
|
+
format!("EINVAL: invalid argument, position {position}"),
|
|
177
|
+
));
|
|
178
|
+
}
|
|
179
|
+
if position < 0.0 {
|
|
180
|
+
return Ok(None);
|
|
181
|
+
}
|
|
182
|
+
Ok(Some(position as u64))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// Whether the descriptor was opened for append.
|
|
186
|
+
///
|
|
187
|
+
/// The table stores only the `File`, so the bit is read back from the descriptor
|
|
188
|
+
/// itself with `F_GETFL`: that reports the flag the kernel actually holds, which
|
|
189
|
+
/// is more reliable than tracking a second copy beside the table.
|
|
190
|
+
#[cfg(unix)]
|
|
191
|
+
fn appends(file: &File) -> bool {
|
|
192
|
+
use std::os::unix::io::AsRawFd as _;
|
|
193
|
+
// SAFETY: `file` owns a valid descriptor for the duration of the call; `F_GETFL` takes no argument.
|
|
194
|
+
let flags = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETFL) };
|
|
195
|
+
flags >= 0 && flags & libc::O_APPEND != 0
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// Windows keeps the append bit inside the handle's granted access, with no
|
|
199
|
+
/// portable way to read it back, so the answer is "no" here. Cursor writes still
|
|
200
|
+
/// append — `append(true)` opens with `FILE_APPEND_DATA`, which does that in the
|
|
201
|
+
/// kernel — and a write with an explicit offset lands at that offset, which is
|
|
202
|
+
/// what a caller using `pwrite` on Windows would expect.
|
|
203
|
+
#[cfg(not(unix))]
|
|
204
|
+
fn appends(_file: &File) -> bool {
|
|
205
|
+
false
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
fn read_at(file: &mut File, buffer: &mut [u8], offset: u64, id: u32) -> Result<usize, ModuleError> {
|
|
209
|
+
let mut filled = 0;
|
|
210
|
+
while filled < buffer.len() {
|
|
211
|
+
match positioned_read(file, &mut buffer[filled..], offset + filled as u64) {
|
|
212
|
+
Ok(0) => break,
|
|
213
|
+
Ok(count) => filled += count,
|
|
214
|
+
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
|
|
215
|
+
Err(error) => return Err(io_error(error, id)),
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
Ok(filled)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fn read_cursor(file: &mut File, buffer: &mut [u8], id: u32) -> Result<usize, ModuleError> {
|
|
222
|
+
let mut filled = 0;
|
|
223
|
+
while filled < buffer.len() {
|
|
224
|
+
match file.read(&mut buffer[filled..]) {
|
|
225
|
+
Ok(0) => break,
|
|
226
|
+
Ok(count) => filled += count,
|
|
227
|
+
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
|
|
228
|
+
Err(error) => return Err(io_error(error, id)),
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
Ok(filled)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fn write_at(file: &mut File, bytes: &[u8], offset: u64, id: u32) -> Result<usize, ModuleError> {
|
|
235
|
+
let mut written = 0;
|
|
236
|
+
while written < bytes.len() {
|
|
237
|
+
match positioned_write(file, &bytes[written..], offset + written as u64) {
|
|
238
|
+
// A regular file does not write zero bytes for a non-empty buffer;
|
|
239
|
+
// treating it as `EIO` keeps the loop from spinning forever.
|
|
240
|
+
Ok(0) => return Err(errors::invalid("EIO", format!("EIO: i/o error, {id}"))),
|
|
241
|
+
Ok(count) => written += count,
|
|
242
|
+
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
|
|
243
|
+
Err(error) => return Err(io_error(error, id)),
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
Ok(written)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fn write_cursor(file: &mut File, bytes: &[u8], id: u32) -> Result<usize, ModuleError> {
|
|
250
|
+
file.write_all(bytes).map_err(|error| io_error(error, id))?;
|
|
251
|
+
Ok(bytes.len())
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#[cfg(unix)]
|
|
255
|
+
fn positioned_read(file: &mut File, buffer: &mut [u8], offset: u64) -> std::io::Result<usize> {
|
|
256
|
+
use std::os::unix::fs::FileExt as _;
|
|
257
|
+
// `read_at` takes `&self`, but the call sites hold `&mut File` so the
|
|
258
|
+
// Windows branch — which has to seek — shares one signature.
|
|
259
|
+
(&*file).read_at(buffer, offset)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/// Windows' positional read, with the cursor left where it was.
|
|
263
|
+
///
|
|
264
|
+
/// `FileExt::seek_read` is documented as not affecting the cursor, but on a
|
|
265
|
+
/// synchronous handle — which is what `File::open` gives — the underlying
|
|
266
|
+
/// `ReadFile` with an `OVERLAPPED` offset keeps the file position as a side
|
|
267
|
+
/// effect. Node's positional forms leave the descriptor alone, and so does the
|
|
268
|
+
/// Unix branch above, so the position is saved and restored here. Without this a
|
|
269
|
+
/// positioned read moved the cursor, and a following cursor read started from the
|
|
270
|
+
/// wrong place.
|
|
271
|
+
#[cfg(windows)]
|
|
272
|
+
fn positioned_read(file: &mut File, buffer: &mut [u8], offset: u64) -> std::io::Result<usize> {
|
|
273
|
+
use std::io::Seek as _;
|
|
274
|
+
use std::os::windows::fs::FileExt as _;
|
|
275
|
+
let saved = file.stream_position();
|
|
276
|
+
let result = file.seek_read(buffer, offset);
|
|
277
|
+
if let Ok(position) = saved {
|
|
278
|
+
let _ = file.seek(std::io::SeekFrom::Start(position));
|
|
279
|
+
}
|
|
280
|
+
result
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
#[cfg(unix)]
|
|
284
|
+
fn positioned_write(file: &mut File, bytes: &[u8], offset: u64) -> std::io::Result<usize> {
|
|
285
|
+
use std::os::unix::fs::FileExt as _;
|
|
286
|
+
(&*file).write_at(bytes, offset)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// Windows' positional write, with the cursor left where it was. Same reason as
|
|
290
|
+
/// `positioned_read` above.
|
|
291
|
+
#[cfg(windows)]
|
|
292
|
+
fn positioned_write(file: &mut File, bytes: &[u8], offset: u64) -> std::io::Result<usize> {
|
|
293
|
+
use std::io::Seek as _;
|
|
294
|
+
use std::os::windows::fs::FileExt as _;
|
|
295
|
+
let saved = file.stream_position();
|
|
296
|
+
let result = file.seek_write(bytes, offset);
|
|
297
|
+
if let Ok(position) = saved {
|
|
298
|
+
let _ = file.seek(std::io::SeekFrom::Start(position));
|
|
299
|
+
}
|
|
300
|
+
result
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/// The generated `StatResult`, filled from `std::fs::Metadata`.
|
|
304
|
+
///
|
|
305
|
+
/// `files::stat` builds the same shape for a path. The two are deliberately
|
|
306
|
+
/// separate: `files` and `handles` are independent, and twenty duplicated lines
|
|
307
|
+
/// cost less than one file reaching into the other's internals.
|
|
308
|
+
fn stat_result(metadata: &std::fs::Metadata) -> StatResult {
|
|
309
|
+
let file_type = metadata.file_type();
|
|
310
|
+
let kind = if file_type.is_file() {
|
|
311
|
+
kind::FILE
|
|
312
|
+
} else if file_type.is_dir() {
|
|
313
|
+
kind::DIRECTORY
|
|
314
|
+
} else if file_type.is_symlink() {
|
|
315
|
+
kind::SYMLINK
|
|
316
|
+
} else {
|
|
317
|
+
kind::OTHER
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// Node's `Stats` names the Unix inode fields even on Windows, where they
|
|
321
|
+
// have no meaning; zero is what Node reports there too.
|
|
322
|
+
#[cfg(unix)]
|
|
323
|
+
let (mode, dev, ino, nlink, uid, gid) = {
|
|
324
|
+
use std::os::unix::fs::MetadataExt as _;
|
|
325
|
+
(
|
|
326
|
+
metadata.mode() as f64,
|
|
327
|
+
metadata.dev() as f64,
|
|
328
|
+
metadata.ino() as f64,
|
|
329
|
+
metadata.nlink() as f64,
|
|
330
|
+
metadata.uid() as f64,
|
|
331
|
+
metadata.gid() as f64,
|
|
332
|
+
)
|
|
333
|
+
};
|
|
334
|
+
#[cfg(not(unix))]
|
|
335
|
+
let (mode, dev, ino, nlink, uid, gid) = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
|
|
336
|
+
|
|
337
|
+
// `ctime` is the inode change time on Unix; Windows has no equivalent, and
|
|
338
|
+
// its creation time is the closest thing it has.
|
|
339
|
+
#[cfg(unix)]
|
|
340
|
+
let ctime_ms = {
|
|
341
|
+
use std::os::unix::fs::MetadataExt as _;
|
|
342
|
+
metadata.ctime() as f64 * 1000.0 + metadata.ctime_nsec() as f64 / 1_000_000.0
|
|
343
|
+
};
|
|
344
|
+
#[cfg(not(unix))]
|
|
345
|
+
let ctime_ms = metadata
|
|
346
|
+
.created()
|
|
347
|
+
.map(millis)
|
|
348
|
+
.unwrap_or_else(|_| metadata.modified().map(millis).unwrap_or(0.0));
|
|
349
|
+
|
|
350
|
+
// A file system that cannot report creation gives 0, as Node does, rather
|
|
351
|
+
// than pretending the modification time is the birth time.
|
|
352
|
+
StatResult {
|
|
353
|
+
size: metadata.len() as f64,
|
|
354
|
+
mode,
|
|
355
|
+
kind,
|
|
356
|
+
atime_ms: metadata.accessed().map(millis).unwrap_or(0.0),
|
|
357
|
+
mtime_ms: metadata.modified().map(millis).unwrap_or(0.0),
|
|
358
|
+
ctime_ms,
|
|
359
|
+
birthtime_ms: metadata.created().map(millis).unwrap_or(0.0),
|
|
360
|
+
dev,
|
|
361
|
+
ino,
|
|
362
|
+
nlink,
|
|
363
|
+
uid,
|
|
364
|
+
gid,
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
#[cfg(test)]
|
|
369
|
+
mod tests {
|
|
370
|
+
use std::path::Path;
|
|
371
|
+
use std::sync::Arc;
|
|
372
|
+
|
|
373
|
+
use rn_gpui_module::{EventEmitter, ModuleInstance, Value};
|
|
374
|
+
use tempfile::TempDir;
|
|
375
|
+
|
|
376
|
+
use super::*;
|
|
377
|
+
use crate::native_fs_module;
|
|
378
|
+
|
|
379
|
+
/// The async trait methods never actually suspend — they call these helpers
|
|
380
|
+
/// directly — so a single-poll executor drives the generated glue without a
|
|
381
|
+
/// runtime.
|
|
382
|
+
fn block_on<F: std::future::Future>(future: F) -> F::Output {
|
|
383
|
+
let mut future = std::pin::pin!(future);
|
|
384
|
+
let waker = std::task::Waker::noop();
|
|
385
|
+
let mut context = std::task::Context::from_waker(waker);
|
|
386
|
+
loop {
|
|
387
|
+
match future.as_mut().poll(&mut context) {
|
|
388
|
+
std::task::Poll::Ready(value) => return value,
|
|
389
|
+
std::task::Poll::Pending => std::thread::yield_now(),
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/// `NativeFsEvents` has a private emitter and no constructor, so a test
|
|
395
|
+
/// cannot call `RNFsModule::create` itself. Building the instance through the
|
|
396
|
+
/// descriptor the bridge uses is the supported way in, and it exercises the
|
|
397
|
+
/// generated glue as a bonus.
|
|
398
|
+
fn module() -> Arc<dyn ModuleInstance> {
|
|
399
|
+
(native_fs_module::<crate::RNFsModule>().create)(EventEmitter::new(1))
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
fn index(name: &str) -> usize {
|
|
403
|
+
native_fs_module::<crate::RNFsModule>()
|
|
404
|
+
.methods
|
|
405
|
+
.iter()
|
|
406
|
+
.position(|method| method.name == name)
|
|
407
|
+
.unwrap_or_else(|| panic!("the spec has no method {name}"))
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
fn call(
|
|
411
|
+
module: &Arc<dyn ModuleInstance>,
|
|
412
|
+
name: &str,
|
|
413
|
+
args: Vec<Value>,
|
|
414
|
+
) -> Result<Value, ModuleError> {
|
|
415
|
+
block_on(module.clone().call_promise(index(name), args)?)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
fn text(path: &Path) -> String {
|
|
419
|
+
path.to_string_lossy().into_owned()
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
fn open_fd(module: &Arc<dyn ModuleInstance>, path: &Path, flags_value: i32) -> f64 {
|
|
423
|
+
let value = call(
|
|
424
|
+
module,
|
|
425
|
+
"open",
|
|
426
|
+
vec![
|
|
427
|
+
Value::String(text(path)),
|
|
428
|
+
Value::Number(f64::from(flags_value)),
|
|
429
|
+
Value::Number(0.0),
|
|
430
|
+
],
|
|
431
|
+
)
|
|
432
|
+
.expect("open");
|
|
433
|
+
match value {
|
|
434
|
+
Value::Number(fd) => fd,
|
|
435
|
+
other => panic!("expected a descriptor, got {}", other.type_name()),
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
fn read_bytes(
|
|
440
|
+
module: &Arc<dyn ModuleInstance>,
|
|
441
|
+
fd: f64,
|
|
442
|
+
length: f64,
|
|
443
|
+
position: f64,
|
|
444
|
+
) -> Result<Vec<u8>, ModuleError> {
|
|
445
|
+
match call(
|
|
446
|
+
module,
|
|
447
|
+
"read",
|
|
448
|
+
vec![
|
|
449
|
+
Value::Number(fd),
|
|
450
|
+
Value::Number(length),
|
|
451
|
+
Value::Number(position),
|
|
452
|
+
],
|
|
453
|
+
)? {
|
|
454
|
+
Value::Buffer(bytes) => Ok(bytes),
|
|
455
|
+
other => panic!("expected bytes, got {}", other.type_name()),
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
fn write_bytes(
|
|
460
|
+
module: &Arc<dyn ModuleInstance>,
|
|
461
|
+
fd: f64,
|
|
462
|
+
bytes: &[u8],
|
|
463
|
+
position: f64,
|
|
464
|
+
) -> Result<f64, ModuleError> {
|
|
465
|
+
match call(
|
|
466
|
+
module,
|
|
467
|
+
"write",
|
|
468
|
+
vec![
|
|
469
|
+
Value::Number(fd),
|
|
470
|
+
Value::Buffer(bytes.to_vec()),
|
|
471
|
+
Value::Number(position),
|
|
472
|
+
],
|
|
473
|
+
)? {
|
|
474
|
+
Value::Number(written) => Ok(written),
|
|
475
|
+
other => panic!("expected a count, got {}", other.type_name()),
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
fn stat_size(module: &Arc<dyn ModuleInstance>, fd: f64) -> f64 {
|
|
480
|
+
let value = call(module, "fstat", vec![Value::Number(fd)]).expect("fstat");
|
|
481
|
+
let Value::Object(fields) = value else {
|
|
482
|
+
panic!("expected a stat object");
|
|
483
|
+
};
|
|
484
|
+
fields
|
|
485
|
+
.iter()
|
|
486
|
+
.find(|(name, _)| name == "size")
|
|
487
|
+
.and_then(|(_, value)| match value {
|
|
488
|
+
Value::Number(size) => Some(*size),
|
|
489
|
+
_ => None,
|
|
490
|
+
})
|
|
491
|
+
.expect("a size field")
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
fn close_fd(module: &Arc<dyn ModuleInstance>, fd: f64) -> Result<Value, ModuleError> {
|
|
495
|
+
call(module, "close", vec![Value::Number(fd)])
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/// Open a fresh file containing `contents` and hand back its descriptor.
|
|
499
|
+
fn opened(module: &Arc<dyn ModuleInstance>, dir: &TempDir, contents: &str) -> (String, f64) {
|
|
500
|
+
let path = dir.path().join("data.txt");
|
|
501
|
+
std::fs::write(&path, contents).expect("write the fixture file");
|
|
502
|
+
let fd = open_fd(module, &path, flags::RDWR);
|
|
503
|
+
(text(&path), fd)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
#[test]
|
|
507
|
+
fn a_handle_reads_back_what_it_wrote() {
|
|
508
|
+
let dir = TempDir::new().expect("a temporary directory");
|
|
509
|
+
let path = dir.path().join("note.txt");
|
|
510
|
+
let module = module();
|
|
511
|
+
|
|
512
|
+
let fd = open_fd(&module, &path, flags::WRONLY | flags::CREAT | flags::TRUNC);
|
|
513
|
+
assert_eq!(
|
|
514
|
+
write_bytes(&module, fd, b"hello", -1.0).expect("write"),
|
|
515
|
+
5.0
|
|
516
|
+
);
|
|
517
|
+
assert_eq!(stat_size(&module, fd), 5.0);
|
|
518
|
+
close_fd(&module, fd).expect("close");
|
|
519
|
+
|
|
520
|
+
let fd = open_fd(&module, &path, flags::RDONLY);
|
|
521
|
+
assert_eq!(read_bytes(&module, fd, 5.0, -1.0).expect("read"), b"hello");
|
|
522
|
+
close_fd(&module, fd).expect("close");
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
#[test]
|
|
526
|
+
fn a_positioned_read_leaves_the_cursor_alone() {
|
|
527
|
+
let dir = TempDir::new().expect("a temporary directory");
|
|
528
|
+
let module = module();
|
|
529
|
+
let (_, fd) = opened(&module, &dir, "abcdef");
|
|
530
|
+
|
|
531
|
+
assert_eq!(read_bytes(&module, fd, 2.0, 0.0).expect("read"), b"ab");
|
|
532
|
+
// The cursor never moved, so a cursor read starts at the beginning.
|
|
533
|
+
assert_eq!(read_bytes(&module, fd, 2.0, -1.0).expect("read"), b"ab");
|
|
534
|
+
assert_eq!(read_bytes(&module, fd, 2.0, -1.0).expect("read"), b"cd");
|
|
535
|
+
close_fd(&module, fd).expect("close");
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
#[test]
|
|
539
|
+
fn a_positioned_write_only_touches_its_range() {
|
|
540
|
+
let dir = TempDir::new().expect("a temporary directory");
|
|
541
|
+
let module = module();
|
|
542
|
+
let (path, fd) = opened(&module, &dir, "abcdef");
|
|
543
|
+
|
|
544
|
+
assert_eq!(write_bytes(&module, fd, b"XY", 2.0).expect("write"), 2.0);
|
|
545
|
+
assert_eq!(stat_size(&module, fd), 6.0);
|
|
546
|
+
assert_eq!(std::fs::read(&path).expect("on disk"), b"abXYef");
|
|
547
|
+
close_fd(&module, fd).expect("close");
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
#[test]
|
|
551
|
+
fn reading_past_the_end_is_a_short_read_not_an_error() {
|
|
552
|
+
let dir = TempDir::new().expect("a temporary directory");
|
|
553
|
+
let module = module();
|
|
554
|
+
let (_, fd) = opened(&module, &dir, "abc");
|
|
555
|
+
|
|
556
|
+
// Two bytes asked for, one left: the buffer holds exactly one byte.
|
|
557
|
+
assert_eq!(read_bytes(&module, fd, 2.0, 2.0).expect("read"), b"c");
|
|
558
|
+
// Nothing left at all: an empty buffer, still a success.
|
|
559
|
+
assert_eq!(read_bytes(&module, fd, 8.0, 10.0).expect("read"), b"");
|
|
560
|
+
assert_eq!(read_bytes(&module, fd, 8.0, -1.0).expect("read"), b"abc");
|
|
561
|
+
assert_eq!(read_bytes(&module, fd, 8.0, -1.0).expect("read"), b"");
|
|
562
|
+
close_fd(&module, fd).expect("close");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
#[test]
|
|
566
|
+
fn a_huge_length_only_allocates_what_the_file_holds() {
|
|
567
|
+
let dir = TempDir::new().expect("a temporary directory");
|
|
568
|
+
let module = module();
|
|
569
|
+
let (_, fd) = opened(&module, &dir, "0123456789");
|
|
570
|
+
|
|
571
|
+
// The buffer is capped at what remains, so this returns rather than
|
|
572
|
+
// trying to allocate four gigabytes. Finishing at all is the check: an
|
|
573
|
+
// allocation of `u32::MAX` bytes would abort the test process instead.
|
|
574
|
+
let bytes = read_bytes(&module, fd, u32::MAX as f64, 0.0).expect("read");
|
|
575
|
+
assert_eq!(bytes, b"0123456789");
|
|
576
|
+
close_fd(&module, fd).expect("close");
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
#[test]
|
|
580
|
+
fn appending_ignores_the_position() {
|
|
581
|
+
let dir = TempDir::new().expect("a temporary directory");
|
|
582
|
+
let path = dir.path().join("log.txt");
|
|
583
|
+
std::fs::write(&path, b"ab").expect("write the fixture file");
|
|
584
|
+
let module = module();
|
|
585
|
+
|
|
586
|
+
let fd = open_fd(&module, &path, flags::APPEND | flags::WRONLY | flags::CREAT);
|
|
587
|
+
write_bytes(&module, fd, b"cd", -1.0).expect("append");
|
|
588
|
+
// An append-mode descriptor appends even when an offset is passed, which
|
|
589
|
+
// is what the kernel does on Linux and what `appendFile` means.
|
|
590
|
+
write_bytes(&module, fd, b"ef", 0.0).expect("append");
|
|
591
|
+
assert_eq!(stat_size(&module, fd), 6.0);
|
|
592
|
+
close_fd(&module, fd).expect("close");
|
|
593
|
+
assert_eq!(std::fs::read(&path).expect("on disk"), b"abcdef");
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
#[test]
|
|
597
|
+
fn truncating_by_descriptor_changes_the_size() {
|
|
598
|
+
let dir = TempDir::new().expect("a temporary directory");
|
|
599
|
+
let module = module();
|
|
600
|
+
let (_, fd) = opened(&module, &dir, "abcdef");
|
|
601
|
+
|
|
602
|
+
call(
|
|
603
|
+
&module,
|
|
604
|
+
"ftruncate",
|
|
605
|
+
vec![Value::Number(fd), Value::Number(3.0)],
|
|
606
|
+
)
|
|
607
|
+
.expect("ftruncate");
|
|
608
|
+
assert_eq!(stat_size(&module, fd), 3.0);
|
|
609
|
+
assert_eq!(read_bytes(&module, fd, 16.0, 0.0).expect("read"), b"abc");
|
|
610
|
+
close_fd(&module, fd).expect("close");
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
#[test]
|
|
614
|
+
fn syncing_a_handle_succeeds() {
|
|
615
|
+
let dir = TempDir::new().expect("a temporary directory");
|
|
616
|
+
let module = module();
|
|
617
|
+
let (_, fd) = opened(&module, &dir, "abc");
|
|
618
|
+
write_bytes(&module, fd, b"d", 3.0).expect("write");
|
|
619
|
+
call(&module, "fsync", vec![Value::Number(fd)]).expect("fsync");
|
|
620
|
+
close_fd(&module, fd).expect("close");
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
#[test]
|
|
624
|
+
fn a_second_close_is_ebadf() {
|
|
625
|
+
let dir = TempDir::new().expect("a temporary directory");
|
|
626
|
+
let module = module();
|
|
627
|
+
let (_, fd) = opened(&module, &dir, "abc");
|
|
628
|
+
|
|
629
|
+
close_fd(&module, fd).expect("the first close");
|
|
630
|
+
let error = close_fd(&module, fd).expect_err("the second close");
|
|
631
|
+
assert_eq!(error.code.as_deref(), Some("EBADF"));
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
#[test]
|
|
635
|
+
fn a_closed_descriptor_is_ebadf_for_every_op() {
|
|
636
|
+
let dir = TempDir::new().expect("a temporary directory");
|
|
637
|
+
let module = module();
|
|
638
|
+
let (_, fd) = opened(&module, &dir, "abc");
|
|
639
|
+
close_fd(&module, fd).expect("close");
|
|
640
|
+
|
|
641
|
+
let error = read_bytes(&module, fd, 1.0, -1.0).expect_err("read");
|
|
642
|
+
assert_eq!(error.code.as_deref(), Some("EBADF"));
|
|
643
|
+
let error = write_bytes(&module, fd, b"x", -1.0).expect_err("write");
|
|
644
|
+
assert_eq!(error.code.as_deref(), Some("EBADF"));
|
|
645
|
+
let error = call(&module, "fstat", vec![Value::Number(fd)]).expect_err("fstat");
|
|
646
|
+
assert_eq!(error.code.as_deref(), Some("EBADF"));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
#[test]
|
|
650
|
+
fn opening_a_missing_file_is_enoent() {
|
|
651
|
+
let dir = TempDir::new().expect("a temporary directory");
|
|
652
|
+
let path = dir.path().join("gone.txt");
|
|
653
|
+
let module = module();
|
|
654
|
+
|
|
655
|
+
let error = call(
|
|
656
|
+
&module,
|
|
657
|
+
"open",
|
|
658
|
+
vec![
|
|
659
|
+
Value::String(text(&path)),
|
|
660
|
+
Value::Number(f64::from(flags::RDONLY)),
|
|
661
|
+
Value::Number(0.0),
|
|
662
|
+
],
|
|
663
|
+
)
|
|
664
|
+
.expect_err("open");
|
|
665
|
+
assert_eq!(error.code.as_deref(), Some("ENOENT"));
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/// A descriptor the table still knows but the kernel rejects: Node says
|
|
669
|
+
/// `EBADF` here, and so must this module, rather than a generic `EIO`.
|
|
670
|
+
#[test]
|
|
671
|
+
fn writing_through_a_read_only_descriptor_is_ebadf() {
|
|
672
|
+
let dir = TempDir::new().expect("a temporary directory");
|
|
673
|
+
let module = module();
|
|
674
|
+
let path = dir.path().join("data.txt");
|
|
675
|
+
std::fs::write(&path, b"abc").expect("write the fixture file");
|
|
676
|
+
let fd = open_fd(&module, &path, flags::RDONLY);
|
|
677
|
+
|
|
678
|
+
let error = write_bytes(&module, fd, b"x", -1.0).expect_err("write");
|
|
679
|
+
assert_eq!(error.code.as_deref(), Some("EBADF"));
|
|
680
|
+
// Reading is still fine, and the file is untouched.
|
|
681
|
+
assert_eq!(read_bytes(&module, fd, 3.0, 0.0).expect("read"), b"abc");
|
|
682
|
+
close_fd(&module, fd).expect("close");
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
#[test]
|
|
686
|
+
fn a_non_finite_position_is_einval() {
|
|
687
|
+
let dir = TempDir::new().expect("a temporary directory");
|
|
688
|
+
let module = module();
|
|
689
|
+
let (_, fd) = opened(&module, &dir, "abc");
|
|
690
|
+
|
|
691
|
+
let error = read_bytes(&module, fd, 1.0, f64::NAN).expect_err("read");
|
|
692
|
+
assert_eq!(error.code.as_deref(), Some("EINVAL"));
|
|
693
|
+
let error = write_bytes(&module, fd, b"x", f64::INFINITY).expect_err("write");
|
|
694
|
+
assert_eq!(error.code.as_deref(), Some("EINVAL"));
|
|
695
|
+
close_fd(&module, fd).expect("close");
|
|
696
|
+
}
|
|
697
|
+
}
|