@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.
@@ -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
+ }