@zenfs/core 1.11.3 → 2.0.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.
Files changed (116) hide show
  1. package/dist/backends/backend.d.ts +19 -15
  2. package/dist/backends/backend.js +31 -15
  3. package/dist/backends/cow.d.ts +20 -30
  4. package/dist/backends/cow.js +52 -142
  5. package/dist/backends/fetch.d.ts +1 -0
  6. package/dist/backends/fetch.js +3 -1
  7. package/dist/backends/index.d.ts +1 -1
  8. package/dist/backends/index.js +1 -1
  9. package/dist/backends/memory.d.ts +5 -7
  10. package/dist/backends/memory.js +2 -3
  11. package/dist/backends/passthrough.d.ts +19 -22
  12. package/dist/backends/passthrough.js +85 -160
  13. package/dist/backends/port.d.ts +207 -0
  14. package/dist/backends/port.js +297 -0
  15. package/dist/backends/single_buffer.d.ts +11 -5
  16. package/dist/backends/single_buffer.js +18 -12
  17. package/dist/backends/store/fs.d.ts +11 -27
  18. package/dist/backends/store/fs.js +67 -91
  19. package/dist/backends/store/store.d.ts +7 -12
  20. package/dist/config.d.ts +1 -10
  21. package/dist/config.js +7 -8
  22. package/dist/context.d.ts +8 -21
  23. package/dist/context.js +33 -10
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +2 -1
  26. package/dist/internal/contexts.d.ts +63 -0
  27. package/dist/internal/contexts.js +15 -0
  28. package/dist/internal/credentials.d.ts +2 -11
  29. package/dist/internal/credentials.js +0 -19
  30. package/dist/internal/devices.d.ts +18 -80
  31. package/dist/internal/devices.js +76 -279
  32. package/dist/internal/file_index.js +3 -3
  33. package/dist/internal/filesystem.d.ts +31 -89
  34. package/dist/internal/filesystem.js +21 -20
  35. package/dist/internal/index.d.ts +0 -1
  36. package/dist/internal/index.js +0 -1
  37. package/dist/internal/index_fs.d.ts +12 -30
  38. package/dist/internal/index_fs.js +23 -55
  39. package/dist/internal/inode.d.ts +147 -9
  40. package/dist/internal/inode.js +333 -25
  41. package/dist/internal/log.d.ts +19 -13
  42. package/dist/internal/log.js +81 -80
  43. package/dist/mixins/async.js +26 -90
  44. package/dist/mixins/mutexed.d.ts +17 -16
  45. package/dist/mixins/mutexed.js +29 -31
  46. package/dist/mixins/readonly.d.ts +7 -6
  47. package/dist/mixins/readonly.js +6 -0
  48. package/dist/mixins/sync.js +8 -8
  49. package/dist/{vfs/path.d.ts → path.d.ts} +3 -4
  50. package/dist/{vfs/path.js → path.js} +6 -9
  51. package/dist/readline.d.ts +134 -0
  52. package/dist/readline.js +623 -0
  53. package/dist/utils.d.ts +4 -35
  54. package/dist/utils.js +8 -73
  55. package/dist/vfs/acl.d.ts +42 -0
  56. package/dist/vfs/acl.js +249 -0
  57. package/dist/vfs/async.d.ts +7 -21
  58. package/dist/vfs/async.js +19 -19
  59. package/dist/vfs/config.d.ts +6 -18
  60. package/dist/vfs/config.js +8 -18
  61. package/dist/vfs/dir.d.ts +3 -3
  62. package/dist/vfs/dir.js +9 -8
  63. package/dist/vfs/file.d.ts +106 -0
  64. package/dist/vfs/file.js +235 -0
  65. package/dist/vfs/flags.d.ts +19 -0
  66. package/dist/vfs/flags.js +62 -0
  67. package/dist/vfs/index.d.ts +4 -10
  68. package/dist/vfs/index.js +4 -13
  69. package/dist/vfs/ioctl.d.ts +87 -0
  70. package/dist/vfs/ioctl.js +304 -0
  71. package/dist/vfs/promises.d.ts +78 -16
  72. package/dist/vfs/promises.js +273 -122
  73. package/dist/vfs/shared.d.ts +7 -26
  74. package/dist/vfs/shared.js +25 -53
  75. package/dist/{stats.d.ts → vfs/stats.d.ts} +14 -28
  76. package/dist/{stats.js → vfs/stats.js} +11 -66
  77. package/dist/vfs/streams.d.ts +1 -0
  78. package/dist/vfs/streams.js +24 -19
  79. package/dist/vfs/sync.d.ts +4 -3
  80. package/dist/vfs/sync.js +143 -128
  81. package/dist/vfs/watchers.d.ts +2 -2
  82. package/dist/vfs/watchers.js +6 -6
  83. package/dist/vfs/xattr.d.ts +116 -0
  84. package/dist/vfs/xattr.js +218 -0
  85. package/package.json +3 -3
  86. package/readme.md +1 -1
  87. package/tests/backend/config.worker.js +4 -1
  88. package/tests/backend/fetch.test.ts +3 -0
  89. package/tests/backend/port.test.ts +21 -35
  90. package/tests/backend/remote.worker.js +4 -1
  91. package/tests/backend/single-buffer.test.ts +24 -0
  92. package/tests/common/context.test.ts +1 -1
  93. package/tests/common/handle.test.ts +17 -12
  94. package/tests/common/path.test.ts +1 -1
  95. package/tests/common/readline.test.ts +104 -0
  96. package/tests/common.ts +4 -19
  97. package/tests/fetch/fetch.ts +1 -1
  98. package/tests/fs/links.test.ts +1 -1
  99. package/tests/fs/permissions.test.ts +7 -6
  100. package/tests/fs/readFile.test.ts +3 -3
  101. package/tests/fs/stat.test.ts +6 -6
  102. package/tests/fs/streams.test.ts +2 -11
  103. package/tests/fs/times.test.ts +1 -1
  104. package/tests/fs/xattr.test.ts +85 -0
  105. package/tests/logs.js +22 -0
  106. package/tests/setup/context.ts +1 -1
  107. package/tests/setup/index.ts +3 -3
  108. package/tests/setup/port.ts +1 -1
  109. package/dist/backends/port/fs.d.ts +0 -84
  110. package/dist/backends/port/fs.js +0 -151
  111. package/dist/backends/port/rpc.d.ts +0 -77
  112. package/dist/backends/port/rpc.js +0 -100
  113. package/dist/backends/store/simple.d.ts +0 -20
  114. package/dist/backends/store/simple.js +0 -13
  115. package/dist/internal/file.d.ts +0 -351
  116. package/dist/internal/file.js +0 -739
@@ -51,37 +51,74 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
51
51
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
52
  });
53
53
  import { Buffer } from 'buffer';
54
- import { _throw } from 'utilium';
55
- import { credentials } from '../internal/credentials.js';
54
+ import { _throw, decodeUTF8, pick } from 'utilium';
55
+ import { defaultContext } from '../internal/contexts.js';
56
56
  import { Errno, ErrnoError } from '../internal/error.js';
57
- import { flagToMode, isAppendable, isExclusive, isReadable, isTruncating, isWriteable, parseFlag } from '../internal/file.js';
57
+ import { hasAccess, InodeFlags, isBlockDevice, isCharacterDevice, isDirectory, isSymbolicLink } from '../internal/inode.js';
58
+ import { dirname, join, parse, resolve } from '../path.js';
58
59
  import '../polyfills.js';
59
- import { BigIntStats } from '../stats.js';
60
- import { decodeUTF8, normalizeMode, normalizeOptions, normalizePath, normalizeTime } from '../utils.js';
61
- import { config } from './config.js';
60
+ import { createInterface } from '../readline.js';
61
+ import { normalizeMode, normalizeOptions, normalizePath, normalizeTime } from '../utils.js';
62
+ import { checkAccess } from './config.js';
62
63
  import * as constants from './constants.js';
63
64
  import { Dir, Dirent } from './dir.js';
64
- import { dirname, join, parse, resolve } from './path.js';
65
- import { _statfs, fd2file, fdMap, file2fd, fixError, resolveMount } from './shared.js';
65
+ import { deleteFD, fromFD, SyncHandle, toFD } from './file.js';
66
+ import * as flags from './flags.js';
67
+ import { _statfs, fixError, resolveMount } from './shared.js';
68
+ import { _chown, BigIntStats, Stats } from './stats.js';
66
69
  import { ReadStream, WriteStream } from './streams.js';
67
- import { FSWatcher, emitChange } from './watchers.js';
70
+ import { emitChange, FSWatcher } from './watchers.js';
68
71
  export * as constants from './constants.js';
69
72
  export class FileHandle {
70
- constructor(fdOrFile, context) {
73
+ /**
74
+ * Get the current file position.
75
+ *
76
+ * We emulate the following bug mentioned in the Node documentation:
77
+ *
78
+ * On Linux, positional writes don't work when the file is opened in append mode.
79
+ * The kernel ignores the position argument and always appends the data to the end of the file.
80
+ * @returns The current file position.
81
+ */
82
+ get position() {
83
+ return this.flag & constants.O_APPEND ? this.inode.size : this._position;
84
+ }
85
+ set position(value) {
86
+ this._position = value;
87
+ }
88
+ constructor(context, fd) {
71
89
  this.context = context;
72
- const isFile = typeof fdOrFile != 'number';
73
- this.fd = isFile ? file2fd(fdOrFile) : fdOrFile;
74
- this.file = isFile ? fdOrFile : fd2file(fdOrFile);
90
+ this.fd = fd;
91
+ /**
92
+ * Current position
93
+ */
94
+ this._position = 0;
95
+ /**
96
+ * Whether the file has changes which have not been written to the FS
97
+ */
98
+ this.dirty = false;
99
+ /**
100
+ * Whether the file is open or closed
101
+ */
102
+ this.closed = false;
103
+ const sync = fromFD(context, fd);
104
+ Object.assign(this, pick(sync, 'path', 'fs', 'internalPath', 'flag', 'inode'));
105
+ }
106
+ get _isSync() {
107
+ return !!(this.flag & constants.O_SYNC || this.inode.flags & InodeFlags.Sync);
75
108
  }
76
109
  _emitChange() {
77
- var _a, _b, _c;
78
- emitChange(this.context, 'change', this.file.path.slice((_c = (_b = (_a = this.context) === null || _a === void 0 ? void 0 : _a.root) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0));
110
+ emitChange(this.context, 'change', this.path);
79
111
  }
80
112
  /**
81
113
  * Asynchronous fchown(2) - Change ownership of a file.
82
114
  */
83
115
  async chown(uid, gid) {
84
- await this.file.chown(uid, gid);
116
+ if (this.closed)
117
+ throw ErrnoError.With('EBADF', this.path, 'chown');
118
+ this.dirty = true;
119
+ _chown(this.inode, uid, gid);
120
+ if (this._isSync)
121
+ await this.sync();
85
122
  this._emitChange();
86
123
  }
87
124
  /**
@@ -92,30 +129,49 @@ export class FileHandle {
92
129
  const numMode = normalizeMode(mode, -1);
93
130
  if (numMode < 0)
94
131
  throw new ErrnoError(Errno.EINVAL, 'Invalid mode');
95
- await this.file.chmod(numMode);
132
+ if (this.closed)
133
+ throw ErrnoError.With('EBADF', this.path, 'chmod');
134
+ this.dirty = true;
135
+ this.inode.mode = (this.inode.mode & (numMode > constants.S_IFMT ? ~constants.S_IFMT : constants.S_IFMT)) | numMode;
136
+ if (this._isSync || numMode > constants.S_IFMT)
137
+ await this.sync();
96
138
  this._emitChange();
97
139
  }
98
140
  /**
99
141
  * Asynchronous fdatasync(2) - synchronize a file's in-core state with storage device.
100
142
  */
101
143
  datasync() {
102
- return this.file.datasync();
144
+ return this.sync();
103
145
  }
104
146
  /**
105
147
  * Asynchronous fsync(2) - synchronize a file's in-core state with the underlying storage device.
106
148
  */
107
- sync() {
108
- return this.file.sync();
149
+ async sync() {
150
+ if (this.closed)
151
+ throw ErrnoError.With('EBADF', this.path, 'sync');
152
+ if (!this.dirty)
153
+ return;
154
+ if (!this.fs.attributes.has('no_write'))
155
+ await this.fs.touch(this.internalPath, this.inode);
156
+ this.dirty = false;
109
157
  }
110
158
  /**
111
159
  * Asynchronous ftruncate(2) - Truncate a file to a specified length.
112
160
  * @param length If not specified, defaults to `0`.
113
161
  */
114
- async truncate(length) {
115
- length || (length = 0);
162
+ async truncate(length = 0) {
163
+ if (this.closed)
164
+ throw ErrnoError.With('EBADF', this.path, 'truncate');
116
165
  if (length < 0)
117
166
  throw new ErrnoError(Errno.EINVAL);
118
- await this.file.truncate(length);
167
+ this.dirty = true;
168
+ if (!(this.flag & constants.O_WRONLY || this.flag & constants.O_RDWR)) {
169
+ throw new ErrnoError(Errno.EPERM, 'File not opened with a writeable mode', this.path, 'truncate');
170
+ }
171
+ this.inode.mtimeMs = Date.now();
172
+ this.inode.size = length;
173
+ if (this._isSync)
174
+ await this.sync();
119
175
  this._emitChange();
120
176
  }
121
177
  /**
@@ -124,7 +180,13 @@ export class FileHandle {
124
180
  * @param mtime The last modified time. If a string is provided, it will be coerced to number.
125
181
  */
126
182
  async utimes(atime, mtime) {
127
- await this.file.utimes(normalizeTime(atime), normalizeTime(mtime));
183
+ if (this.closed)
184
+ throw ErrnoError.With('EBADF', this.path, 'utimes');
185
+ this.dirty = true;
186
+ this.inode.atimeMs = normalizeTime(atime);
187
+ this.inode.mtimeMs = normalizeTime(mtime);
188
+ if (this._isSync)
189
+ await this.sync();
128
190
  this._emitChange();
129
191
  }
130
192
  /**
@@ -138,17 +200,45 @@ export class FileHandle {
138
200
  */
139
201
  async appendFile(data, _options = {}) {
140
202
  const options = normalizeOptions(_options, 'utf8', 'a', 0o644);
141
- const flag = parseFlag(options.flag);
142
- if (!isAppendable(flag)) {
203
+ const flag = flags.parse(options.flag);
204
+ if (!(flag & constants.O_APPEND)) {
143
205
  throw new ErrnoError(Errno.EINVAL, 'Flag passed to appendFile must allow for appending');
144
206
  }
145
207
  if (typeof data != 'string' && !options.encoding) {
146
208
  throw new ErrnoError(Errno.EINVAL, 'Encoding not specified');
147
209
  }
148
210
  const encodedData = typeof data == 'string' ? Buffer.from(data, options.encoding) : data;
149
- await this.file.write(encodedData, 0, encodedData.length);
211
+ await this._write(encodedData, 0, encodedData.length);
150
212
  this._emitChange();
151
213
  }
214
+ /**
215
+ * Read data from the file.
216
+ * @param buffer The buffer that the data will be written to.
217
+ * @param offset The offset within the buffer where writing will start.
218
+ * @param length An integer specifying the number of bytes to read.
219
+ * @param position An integer specifying where to begin reading from in the file.
220
+ * If position is unset, data will be read from the current file position.
221
+ */
222
+ async _read(buffer, offset = 0, length = buffer.byteLength - offset, position = this.position) {
223
+ if (this.closed)
224
+ throw ErrnoError.With('EBADF', this.path, 'read');
225
+ if (this.flag & constants.O_WRONLY)
226
+ throw new ErrnoError(Errno.EPERM, 'File not opened with a readable mode');
227
+ if (!(this.inode.flags & InodeFlags.NoAtime)) {
228
+ this.dirty = true;
229
+ this.inode.atimeMs = Date.now();
230
+ }
231
+ let end = position + length;
232
+ if (!isCharacterDevice(this.inode) && !isBlockDevice(this.inode) && end > this.inode.size) {
233
+ end = position + Math.max(this.inode.size - position, 0);
234
+ }
235
+ this._position = end;
236
+ const uint8 = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
237
+ await this.fs.read(this.internalPath, uint8.subarray(offset, offset + length), position, end);
238
+ if (this._isSync)
239
+ await this.sync();
240
+ return { bytesRead: end - position, buffer };
241
+ }
152
242
  async read(buffer, offset, length, position) {
153
243
  if (typeof offset == 'object' && offset != null) {
154
244
  position = offset.position;
@@ -161,19 +251,19 @@ export class FileHandle {
161
251
  offset = buffer.offset;
162
252
  buffer = buffer.buffer;
163
253
  }
164
- const pos = Number.isSafeInteger(position) ? position : this.file.position;
165
- buffer || (buffer = new Uint8Array((await this.file.stat()).size));
254
+ const pos = Number.isSafeInteger(position) ? position : this.position;
255
+ buffer || (buffer = new Uint8Array(this.inode.size));
166
256
  offset !== null && offset !== void 0 ? offset : (offset = 0);
167
- return this.file.read(buffer, offset, length !== null && length !== void 0 ? length : buffer.byteLength - offset, pos);
257
+ return this._read(buffer, offset, length !== null && length !== void 0 ? length : buffer.byteLength - offset, pos);
168
258
  }
169
259
  async readFile(_options) {
170
260
  const options = normalizeOptions(_options, null, 'r', 0o444);
171
- const flag = parseFlag(options.flag);
172
- if (!isReadable(flag)) {
173
- throw new ErrnoError(Errno.EINVAL, 'Flag passed must allow for reading');
261
+ const flag = flags.parse(options.flag);
262
+ if (flag & constants.O_WRONLY) {
263
+ throw new ErrnoError(Errno.EINVAL, 'Flag passed must allow for reading', this.path, 'readFile');
174
264
  }
175
265
  const { size } = await this.stat();
176
- const { buffer: data } = await this.file.read(new Uint8Array(size), 0, size, 0);
266
+ const { buffer: data } = await this._read(new Uint8Array(size), 0, size, 0);
177
267
  const buffer = Buffer.from(data);
178
268
  return options.encoding ? buffer.toString(options.encoding) : buffer;
179
269
  }
@@ -182,23 +272,72 @@ export class FileHandle {
182
272
  * The handle will not be closed automatically.
183
273
  */
184
274
  readableWebStream(options = {}) {
185
- return this.file.fs.streamRead(this.file.path, {});
275
+ if (this.closed)
276
+ throw ErrnoError.With('EBADF', this.path, 'readableWebStream');
277
+ return this.fs.streamRead(this.internalPath, options);
278
+ }
279
+ /**
280
+ * Not part of the Node.js API!
281
+ *
282
+ * Write file data using a `WritableStream`.
283
+ * The handle will not be closed automatically.
284
+ * @internal
285
+ */
286
+ writableWebStream(options = {}) {
287
+ if (this.closed)
288
+ throw ErrnoError.With('EBADF', this.path, 'writableWebStream');
289
+ if (this.inode.flags & InodeFlags.Immutable)
290
+ throw new ErrnoError(Errno.EPERM, 'File is immutable', this.path, 'writableWebStream');
291
+ return this.fs.streamWrite(this.internalPath, options);
186
292
  }
187
293
  /**
188
- * @todo Implement
294
+ * Creates a readline Interface object that allows reading the file line by line
295
+ * @param options Options for creating a read stream
296
+ * @returns A readline interface for reading the file line by line
189
297
  */
190
298
  readLines(options) {
191
- throw ErrnoError.With('ENOSYS', this.file.path, 'FileHandle.readLines');
299
+ if (this.closed || this.flag & constants.O_WRONLY)
300
+ throw ErrnoError.With('EBADF', this.path, 'readLines');
301
+ return createInterface({ input: this.createReadStream(options), crlfDelay: Infinity });
192
302
  }
193
303
  [Symbol.asyncDispose]() {
194
304
  return this.close();
195
305
  }
196
306
  async stat(opts) {
197
- const stats = await this.file.stat();
198
- if (config.checkAccess && !stats.hasAccess(constants.R_OK, this.context)) {
199
- throw ErrnoError.With('EACCES', this.file.path, 'stat');
307
+ if (this.closed)
308
+ throw ErrnoError.With('EBADF', this.path, 'stat');
309
+ if (checkAccess && !hasAccess(this.context, this.inode, constants.R_OK)) {
310
+ throw ErrnoError.With('EACCES', this.path, 'stat');
200
311
  }
201
- return (opts === null || opts === void 0 ? void 0 : opts.bigint) ? new BigIntStats(stats) : stats;
312
+ return (opts === null || opts === void 0 ? void 0 : opts.bigint) ? new BigIntStats(this.inode) : new Stats(this.inode);
313
+ }
314
+ /**
315
+ * Write buffer to the file.
316
+ * @param buffer Uint8Array containing the data to write to the file.
317
+ * @param offset Offset in the buffer to start reading data from.
318
+ * @param length The amount of bytes to write to the file.
319
+ * @param position Offset from the beginning of the file where this data should be written.
320
+ * If position is null, the data will be written at the current position.
321
+ */
322
+ async _write(buffer, offset = 0, length = buffer.byteLength - offset, position = this.position) {
323
+ if (this.closed)
324
+ throw ErrnoError.With('EBADF', this.path, 'write');
325
+ if (this.inode.flags & InodeFlags.Immutable)
326
+ throw new ErrnoError(Errno.EPERM, 'File is immutable', this.path, 'write');
327
+ if (!(this.flag & constants.O_WRONLY || this.flag & constants.O_RDWR))
328
+ throw new ErrnoError(Errno.EPERM, 'File not opened with a writeable mode', this.path, 'write');
329
+ this.dirty = true;
330
+ const end = position + length;
331
+ const slice = buffer.subarray(offset, offset + length);
332
+ if (!isCharacterDevice(this.inode) && !isBlockDevice(this.inode) && end > this.inode.size)
333
+ this.inode.size = end;
334
+ this.inode.mtimeMs = Date.now();
335
+ this.inode.ctimeMs = Date.now();
336
+ this._position = position + slice.byteLength;
337
+ await this.fs.write(this.internalPath, slice, position);
338
+ if (this._isSync)
339
+ await this.sync();
340
+ return slice.byteLength;
202
341
  }
203
342
  /**
204
343
  * Asynchronously writes `string` to the file.
@@ -225,8 +364,8 @@ export class FileHandle {
225
364
  length = typeof lenOrEnc == 'number' ? lenOrEnc : buffer.byteLength;
226
365
  position = typeof position === 'number' ? position : null;
227
366
  }
228
- position !== null && position !== void 0 ? position : (position = this.file.position);
229
- const bytesWritten = await this.file.write(buffer, offset, length, position);
367
+ position !== null && position !== void 0 ? position : (position = this.position);
368
+ const bytesWritten = await this._write(buffer, offset, length, position);
230
369
  this._emitChange();
231
370
  return { buffer: data, bytesWritten };
232
371
  }
@@ -242,23 +381,36 @@ export class FileHandle {
242
381
  */
243
382
  async writeFile(data, _options = {}) {
244
383
  const options = normalizeOptions(_options, 'utf8', 'w', 0o644);
245
- const flag = parseFlag(options.flag);
246
- if (!isWriteable(flag)) {
247
- throw new ErrnoError(Errno.EINVAL, 'Flag passed must allow for writing');
384
+ const flag = flags.parse(options.flag);
385
+ if (!(flag & constants.O_WRONLY || flag & constants.O_RDWR)) {
386
+ throw new ErrnoError(Errno.EINVAL, 'Flag passed must allow for writing', this.path, 'writeFile');
248
387
  }
249
388
  if (typeof data != 'string' && !options.encoding) {
250
389
  throw new ErrnoError(Errno.EINVAL, 'Encoding not specified');
251
390
  }
252
391
  const encodedData = typeof data == 'string' ? Buffer.from(data, options.encoding) : data;
253
- await this.file.write(encodedData, 0, encodedData.length, 0);
392
+ await this._write(encodedData, 0, encodedData.length, 0);
254
393
  this._emitChange();
255
394
  }
256
395
  /**
257
396
  * Asynchronous close(2) - close a `FileHandle`.
258
397
  */
259
398
  async close() {
260
- await this.file.close();
261
- fdMap.delete(this.fd);
399
+ if (this.closed)
400
+ throw ErrnoError.With('EBADF', this.path, 'close');
401
+ await this.sync();
402
+ this.dispose();
403
+ deleteFD(this.context, this.fd);
404
+ }
405
+ /**
406
+ * Cleans up. This will *not* sync the file data to the FS
407
+ */
408
+ dispose(force) {
409
+ if (this.closed)
410
+ throw ErrnoError.With('EBADF', this.path, 'dispose');
411
+ if (this.dirty && !force)
412
+ throw ErrnoError.With('EBUSY', this.path, 'dispose');
413
+ this.closed = true;
262
414
  }
263
415
  /**
264
416
  * Asynchronous `writev`. Writes from multiple buffers.
@@ -268,7 +420,7 @@ export class FileHandle {
268
420
  */
269
421
  async writev(buffers, position) {
270
422
  if (typeof position == 'number')
271
- this.file.position = position;
423
+ this.position = position;
272
424
  let bytesWritten = 0;
273
425
  for (const buffer of buffers) {
274
426
  bytesWritten += (await this.write(buffer)).bytesWritten;
@@ -283,7 +435,7 @@ export class FileHandle {
283
435
  */
284
436
  async readv(buffers, position) {
285
437
  if (typeof position == 'number')
286
- this.file.position = position;
438
+ this.position = position;
287
439
  let bytesRead = 0;
288
440
  for (const buffer of buffers) {
289
441
  bytesRead += (await this.read(buffer)).bytesRead;
@@ -295,6 +447,8 @@ export class FileHandle {
295
447
  * @param options Options for the readable stream
296
448
  */
297
449
  createReadStream(options = {}) {
450
+ if (this.closed || this.flag & constants.O_WRONLY)
451
+ throw ErrnoError.With('EBADF', this.path, 'createReadStream');
298
452
  return new ReadStream(options, this);
299
453
  }
300
454
  /**
@@ -302,6 +456,10 @@ export class FileHandle {
302
456
  * @param options Options for the writeable stream.
303
457
  */
304
458
  createWriteStream(options = {}) {
459
+ if (this.closed)
460
+ throw ErrnoError.With('EBADF', this.path, 'createWriteStream');
461
+ if (this.inode.flags & InodeFlags.Immutable)
462
+ throw new ErrnoError(Errno.EPERM, 'File is immutable', this.path, 'createWriteStream');
305
463
  return new WriteStream(options, this);
306
464
  }
307
465
  }
@@ -310,7 +468,7 @@ export async function rename(oldPath, newPath) {
310
468
  newPath = normalizePath(newPath);
311
469
  const src = resolveMount(oldPath, this);
312
470
  const dst = resolveMount(newPath, this);
313
- if (config.checkAccess && !(await stat.call(this, dirname(oldPath))).hasAccess(constants.W_OK, this)) {
471
+ if (checkAccess && !(await stat.call(this, dirname(oldPath))).hasAccess(constants.W_OK, this)) {
314
472
  throw ErrnoError.With('EACCES', oldPath, 'rename');
315
473
  }
316
474
  try {
@@ -349,10 +507,10 @@ export async function stat(path, options) {
349
507
  const { fs, path: resolved } = resolveMount(await realpath.call(this, path), this);
350
508
  try {
351
509
  const stats = await fs.stat(resolved);
352
- if (config.checkAccess && !stats.hasAccess(constants.R_OK, this)) {
510
+ if (checkAccess && !hasAccess(this, stats, constants.R_OK)) {
353
511
  throw ErrnoError.With('EACCES', resolved, 'stat');
354
512
  }
355
- return (options === null || options === void 0 ? void 0 : options.bigint) ? new BigIntStats(stats) : stats;
513
+ return (options === null || options === void 0 ? void 0 : options.bigint) ? new BigIntStats(stats) : new Stats(stats);
356
514
  }
357
515
  catch (e) {
358
516
  throw fixError(e, { [resolved]: path });
@@ -364,7 +522,10 @@ export async function lstat(path, options) {
364
522
  const { fs, path: resolved } = resolveMount(path, this);
365
523
  try {
366
524
  const stats = await fs.stat(resolved);
367
- return (options === null || options === void 0 ? void 0 : options.bigint) ? new BigIntStats(stats) : stats;
525
+ if (checkAccess && !hasAccess(this, stats, constants.R_OK)) {
526
+ throw ErrnoError.With('EACCES', resolved, 'lstat');
527
+ }
528
+ return (options === null || options === void 0 ? void 0 : options.bigint) ? new BigIntStats(stats) : new Stats(stats);
368
529
  }
369
530
  catch (e) {
370
531
  throw fixError(e, { [resolved]: path });
@@ -393,7 +554,7 @@ export async function unlink(path) {
393
554
  path = normalizePath(path);
394
555
  const { fs, path: resolved } = resolveMount(path, this);
395
556
  try {
396
- if (config.checkAccess && !(await fs.stat(resolved)).hasAccess(constants.W_OK, this)) {
557
+ if (checkAccess && !hasAccess(this, await fs.stat(resolved), constants.W_OK)) {
397
558
  throw ErrnoError.With('EACCES', resolved, 'unlink');
398
559
  }
399
560
  await fs.unlink(resolved);
@@ -404,16 +565,6 @@ export async function unlink(path) {
404
565
  }
405
566
  }
406
567
  unlink;
407
- /**
408
- * Manually apply setuid/setgid.
409
- */
410
- async function applySetId(file, uid, gid) {
411
- if (file.fs.attributes.has('setid'))
412
- return;
413
- const parent = await file.fs.stat(dirname(file.path));
414
- await file.chown(parent.mode & constants.S_ISUID ? parent.uid : uid, // manually apply setuid/setgid
415
- parent.mode & constants.S_ISGID ? parent.gid : gid);
416
- }
417
568
  /**
418
569
  * Opens a file. This helper handles the complexity of file flags.
419
570
  * @internal
@@ -421,47 +572,48 @@ async function applySetId(file, uid, gid) {
421
572
  async function _open($, path, opt) {
422
573
  var _a;
423
574
  path = normalizePath(path);
424
- const mode = normalizeMode(opt.mode, 0o644), flag = parseFlag(opt.flag);
575
+ const mode = normalizeMode(opt.mode, 0o644), flag = flags.parse(opt.flag);
425
576
  const { fullPath, fs, path: resolved, stats } = await _resolve($, path.toString(), opt.preserveSymlinks);
426
577
  if (!stats) {
427
- if ((!isWriteable(flag) && !isAppendable(flag)) || flag == 'r+') {
578
+ if (!(flag & constants.O_CREAT)) {
428
579
  throw ErrnoError.With('ENOENT', fullPath, '_open');
429
580
  }
430
581
  // Create the file
431
582
  const parentStats = await fs.stat(dirname(resolved));
432
- if (config.checkAccess && !parentStats.hasAccess(constants.W_OK, $)) {
583
+ if (checkAccess && !hasAccess($, parentStats, constants.W_OK)) {
433
584
  throw ErrnoError.With('EACCES', dirname(fullPath), '_open');
434
585
  }
435
- if (!parentStats.isDirectory()) {
586
+ if (!isDirectory(parentStats)) {
436
587
  throw ErrnoError.With('ENOTDIR', dirname(fullPath), '_open');
437
588
  }
438
- const { euid: uid, egid: gid } = (_a = $ === null || $ === void 0 ? void 0 : $.credentials) !== null && _a !== void 0 ? _a : credentials;
439
- const file = await fs.createFile(resolved, flag, mode, { uid, gid });
440
- await applySetId(file, uid, gid);
441
- return new FileHandle(file, $);
442
- }
443
- if (config.checkAccess && !stats.hasAccess(flagToMode(flag), $)) {
589
+ const { euid: uid, egid: gid } = (_a = $ === null || $ === void 0 ? void 0 : $.credentials) !== null && _a !== void 0 ? _a : defaultContext.credentials;
590
+ const inode = await fs.createFile(resolved, {
591
+ mode,
592
+ uid: parentStats.mode & constants.S_ISUID ? parentStats.uid : uid,
593
+ gid: parentStats.mode & constants.S_ISGID ? parentStats.gid : gid,
594
+ });
595
+ return new FileHandle($, toFD(new SyncHandle($, path, fs, resolved, flag, inode)));
596
+ }
597
+ if (checkAccess && !hasAccess($, stats, flags.toMode(flag))) {
444
598
  throw ErrnoError.With('EACCES', fullPath, '_open');
445
599
  }
446
- if (isExclusive(flag)) {
600
+ if (flag & constants.O_EXCL)
447
601
  throw ErrnoError.With('EEXIST', fullPath, '_open');
448
- }
449
- const handle = new FileHandle(await fs.openFile(resolved, flag), $);
602
+ const handle = new FileHandle($, toFD(new SyncHandle($, path, fs, resolved, flag, stats)));
450
603
  /*
451
604
  In a previous implementation, we deleted the file and
452
605
  re-created it. However, this created a race condition if another
453
606
  asynchronous request was trying to read the file, as the file
454
607
  would not exist for a small period of time.
455
608
  */
456
- if (isTruncating(flag)) {
609
+ if (flag & constants.O_TRUNC)
457
610
  await handle.truncate(0);
458
- }
459
611
  return handle;
460
612
  }
461
613
  /**
462
614
  * Asynchronous file open.
463
- * @see http://www.manpagez.com/man/2/open/
464
- * @param flag Handles the complexity of the various file modes. See its API for more details.
615
+ * @see https://nodejs.org/api/fs.html#fspromisesopenpath-flags-mode
616
+ * @param flag {@link https://nodejs.org/api/fs.html#file-system-flags}
465
617
  * @param mode Mode to use to open the file. Can be ignored if the filesystem doesn't support permissions.
466
618
  */
467
619
  export async function open(path, flag = 'r', mode = 0o644) {
@@ -471,7 +623,7 @@ open;
471
623
  export async function readFile(path, _options) {
472
624
  const env_2 = { stack: [], error: void 0, hasError: false };
473
625
  try {
474
- const options = normalizeOptions(_options, null, 'r', 0o644);
626
+ const options = normalizeOptions(_options, null, 'r', 0o444);
475
627
  const handle = __addDisposableResource(env_2, typeof path == 'object' && 'fd' in path ? path : await open.call(this, path, options.flag, options.mode), true);
476
628
  return await handle.readFile(options);
477
629
  }
@@ -501,7 +653,7 @@ export async function writeFile(path, data, _options) {
501
653
  const handle = __addDisposableResource(env_3, path instanceof FileHandle ? path : await open.call(this, path.toString(), options.flag, options.mode), true);
502
654
  const _data = typeof data == 'string' ? data : data instanceof DataView ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : data;
503
655
  if (typeof _data != 'string' && !(_data instanceof Uint8Array)) {
504
- throw new ErrnoError(Errno.EINVAL, 'The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received ' + typeof data, handle.file.path, 'writeFile');
656
+ throw new ErrnoError(Errno.EINVAL, 'The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received ' + typeof data, handle.path, 'writeFile');
505
657
  }
506
658
  await handle.writeFile(_data, options);
507
659
  }
@@ -526,8 +678,8 @@ export async function appendFile(path, data, _options) {
526
678
  const env_4 = { stack: [], error: void 0, hasError: false };
527
679
  try {
528
680
  const options = normalizeOptions(_options, 'utf8', 'a', 0o644);
529
- const flag = parseFlag(options.flag);
530
- if (!isAppendable(flag)) {
681
+ const flag = flags.parse(options.flag);
682
+ if (!(flag & constants.O_APPEND)) {
531
683
  throw new ErrnoError(Errno.EINVAL, 'Flag passed to appendFile must allow for appending');
532
684
  }
533
685
  if (typeof data != 'string' && !options.encoding) {
@@ -548,21 +700,17 @@ export async function appendFile(path, data, _options) {
548
700
  }
549
701
  }
550
702
  appendFile;
551
- // DIRECTORY-ONLY METHODS
552
703
  export async function rmdir(path) {
553
704
  path = await realpath.call(this, path);
554
705
  const { fs, path: resolved } = resolveMount(path, this);
555
706
  try {
556
707
  const stats = await fs.stat(resolved);
557
- if (!stats) {
708
+ if (!stats)
558
709
  throw ErrnoError.With('ENOENT', path, 'rmdir');
559
- }
560
- if (!stats.isDirectory()) {
710
+ if (!isDirectory(stats))
561
711
  throw ErrnoError.With('ENOTDIR', resolved, 'rmdir');
562
- }
563
- if (config.checkAccess && !stats.hasAccess(constants.W_OK, this)) {
712
+ if (checkAccess && !hasAccess(this, stats, constants.W_OK))
564
713
  throw ErrnoError.With('EACCES', resolved, 'rmdir');
565
- }
566
714
  await fs.rmdir(resolved);
567
715
  emitChange(this, 'rename', path.toString());
568
716
  }
@@ -573,20 +721,26 @@ export async function rmdir(path) {
573
721
  rmdir;
574
722
  export async function mkdir(path, options) {
575
723
  var _a, _b;
576
- const { euid: uid, egid: gid } = (_a = this === null || this === void 0 ? void 0 : this.credentials) !== null && _a !== void 0 ? _a : credentials;
724
+ const { euid: uid, egid: gid } = (_a = this === null || this === void 0 ? void 0 : this.credentials) !== null && _a !== void 0 ? _a : defaultContext.credentials;
577
725
  options = typeof options === 'object' ? options : { mode: options };
578
726
  const mode = normalizeMode(options === null || options === void 0 ? void 0 : options.mode, 0o777);
579
727
  path = await realpath.call(this, path);
580
728
  const { fs, path: resolved, root } = resolveMount(path, this);
581
729
  const errorPaths = { [resolved]: path };
730
+ const __create = async (path, parent) => {
731
+ if (checkAccess && !hasAccess(this, parent, constants.W_OK))
732
+ throw ErrnoError.With('EACCES', dirname(path), 'mkdir');
733
+ const inode = await fs.mkdir(path, {
734
+ mode,
735
+ uid: parent.mode & constants.S_ISUID ? parent.uid : uid,
736
+ gid: parent.mode & constants.S_ISGID ? parent.gid : gid,
737
+ });
738
+ emitChange(this, 'rename', path);
739
+ return inode;
740
+ };
582
741
  try {
583
742
  if (!(options === null || options === void 0 ? void 0 : options.recursive)) {
584
- if (config.checkAccess && !(await fs.stat(dirname(resolved))).hasAccess(constants.W_OK, this)) {
585
- throw ErrnoError.With('EACCES', dirname(resolved), 'mkdir');
586
- }
587
- await fs.mkdir(resolved, mode, { uid, gid });
588
- await applySetId(await fs.openFile(resolved, 'r+'), uid, gid);
589
- emitChange(this, 'rename', path.toString());
743
+ await __create(resolved, await fs.stat(dirname(resolved)));
590
744
  return;
591
745
  }
592
746
  const dirs = [];
@@ -594,13 +748,11 @@ export async function mkdir(path, options) {
594
748
  dirs.unshift(dir);
595
749
  errorPaths[dir] = origDir;
596
750
  }
597
- for (const dir of dirs) {
598
- if (config.checkAccess && !(await fs.stat(dirname(dir))).hasAccess(constants.W_OK, this)) {
599
- throw ErrnoError.With('EACCES', dirname(dir), 'mkdir');
600
- }
601
- await fs.mkdir(dir, mode, { uid, gid });
602
- await applySetId(await fs.openFile(dir, 'r+'), uid, gid);
603
- emitChange(this, 'rename', dir);
751
+ if (!dirs.length)
752
+ return;
753
+ const stats = [await fs.stat(dirname(dirs[0]))];
754
+ for (const [i, dir] of dirs.entries()) {
755
+ stats.push(await __create(dir, stats[i]));
604
756
  }
605
757
  return root.length == 1 ? dirs[0] : (_b = dirs[0]) === null || _b === void 0 ? void 0 : _b.slice(root.length);
606
758
  }
@@ -617,12 +769,10 @@ export async function readdir(path, options) {
617
769
  if (!stats) {
618
770
  throw ErrnoError.With('ENOENT', path, 'readdir');
619
771
  }
620
- if (config.checkAccess && !stats.hasAccess(constants.R_OK, this)) {
772
+ if (checkAccess && !hasAccess(this, stats, constants.R_OK))
621
773
  throw ErrnoError.With('EACCES', path, 'readdir');
622
- }
623
- if (!stats.isDirectory()) {
774
+ if (!isDirectory(stats))
624
775
  throw ErrnoError.With('ENOTDIR', path, 'readdir');
625
- }
626
776
  const entries = await fs.readdir(resolved).catch((e) => _throw(fixError(e, { [resolved]: path })));
627
777
  const values = [];
628
778
  const addEntry = async (entry) => {
@@ -645,7 +795,7 @@ export async function readdir(path, options) {
645
795
  else {
646
796
  values.push(entry);
647
797
  }
648
- if (!(options === null || options === void 0 ? void 0 : options.recursive) || !(entryStats === null || entryStats === void 0 ? void 0 : entryStats.isDirectory()))
798
+ if (!(options === null || options === void 0 ? void 0 : options.recursive) || !isDirectory(entryStats))
649
799
  return;
650
800
  for (const subEntry of await readdir.call(this, join(path, entry), options)) {
651
801
  if (subEntry instanceof Dirent) {
@@ -674,13 +824,14 @@ export async function link(targetPath, linkPath) {
674
824
  throw ErrnoError.With('EXDEV', linkPath, 'link');
675
825
  }
676
826
  try {
677
- if (config.checkAccess && !(await fs.stat(dirname(targetPath))).hasAccess(constants.R_OK, this)) {
827
+ if (checkAccess && !hasAccess(this, await fs.stat(dirname(path)), constants.R_OK)) {
678
828
  throw ErrnoError.With('EACCES', dirname(path), 'link');
679
829
  }
680
- if (config.checkAccess && !(await stat.call(this, dirname(linkPath))).hasAccess(constants.W_OK, this)) {
830
+ // We need to use the VFS here since the link path may be a mount point
831
+ if (checkAccess && !(await stat.call(this, dirname(linkPath))).hasAccess(constants.W_OK, this)) {
681
832
  throw ErrnoError.With('EACCES', dirname(linkPath), 'link');
682
833
  }
683
- if (config.checkAccess && !(await fs.stat(path)).hasAccess(constants.R_OK, this)) {
834
+ if (checkAccess && !hasAccess(this, await fs.stat(path), constants.R_OK)) {
684
835
  throw ErrnoError.With('EACCES', path, 'link');
685
836
  }
686
837
  return await fs.link(path, link.path);
@@ -707,7 +858,7 @@ export async function symlink(target, path, type = 'file') {
707
858
  throw ErrnoError.With('EEXIST', path, 'symlink');
708
859
  const handle = __addDisposableResource(env_5, await _open(this, path, { flag: 'w+', mode: 0o644, preserveSymlinks: true }), true);
709
860
  await handle.writeFile(normalizePath(target, true));
710
- await handle.file.chmod(constants.S_IFLNK);
861
+ await handle.chmod(constants.S_IFLNK);
711
862
  }
712
863
  catch (e_5) {
713
864
  env_5.error = e_5;
@@ -880,10 +1031,10 @@ async function _resolve($, path, preserveSymlinks) {
880
1031
  const resolved = resolveMount(path, $);
881
1032
  // Stat it to make sure it exists
882
1033
  const stats = await resolved.fs.stat(resolved.path);
883
- if (!stats.isSymbolicLink()) {
1034
+ if (!isSymbolicLink(stats)) {
884
1035
  return { ...resolved, fullPath: path, stats };
885
1036
  }
886
- const target = resolve(dirname(path), (await readlink.call($, path)).toString());
1037
+ const target = resolve.call($, dirname(path), (await readlink.call($, path)).toString());
887
1038
  return await _resolve($, target);
888
1039
  }
889
1040
  catch {
@@ -895,10 +1046,10 @@ async function _resolve($, path, preserveSymlinks) {
895
1046
  const resolved = resolveMount(maybePath, $);
896
1047
  try {
897
1048
  const stats = await resolved.fs.stat(resolved.path);
898
- if (!stats.isSymbolicLink()) {
1049
+ if (!isSymbolicLink(stats)) {
899
1050
  return { ...resolved, fullPath: maybePath, stats };
900
1051
  }
901
- const target = resolve(realDir, (await readlink.call($, maybePath)).toString());
1052
+ const target = resolve.call($, realDir, (await readlink.call($, maybePath)).toString());
902
1053
  return await _resolve($, target);
903
1054
  }
904
1055
  catch (e) {
@@ -959,7 +1110,7 @@ export function watch(filename, options = {}) {
959
1110
  }
960
1111
  watch;
961
1112
  export async function access(path, mode = constants.F_OK) {
962
- if (!config.checkAccess)
1113
+ if (!checkAccess)
963
1114
  return;
964
1115
  const stats = await stat.call(this, path);
965
1116
  if (!stats.hasAccess(mode, this)) {