@zenfs/core 1.2.9 → 1.3.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 (57) hide show
  1. package/dist/backends/fetch.js +1 -1
  2. package/dist/backends/memory.d.ts +1 -2
  3. package/dist/backends/overlay.js +2 -3
  4. package/dist/backends/port/fs.d.ts +2 -15
  5. package/dist/backends/store/fs.d.ts +17 -28
  6. package/dist/backends/store/fs.js +169 -191
  7. package/dist/backends/store/simple.d.ts +20 -21
  8. package/dist/backends/store/simple.js +24 -24
  9. package/dist/backends/store/store.d.ts +22 -23
  10. package/dist/backends/store/store.js +6 -6
  11. package/dist/config.d.ts +8 -0
  12. package/dist/config.js +2 -1
  13. package/dist/devices.d.ts +2 -3
  14. package/dist/emulation/cache.d.ts +40 -31
  15. package/dist/emulation/cache.js +62 -53
  16. package/dist/emulation/index.d.ts +1 -1
  17. package/dist/emulation/index.js +1 -1
  18. package/dist/emulation/promises.js +18 -17
  19. package/dist/emulation/shared.d.ts +6 -0
  20. package/dist/emulation/shared.js +13 -1
  21. package/dist/emulation/sync.d.ts +1 -1
  22. package/dist/emulation/sync.js +16 -15
  23. package/dist/file.d.ts +3 -15
  24. package/dist/file.js +7 -19
  25. package/dist/inode.d.ts +4 -13
  26. package/dist/inode.js +22 -29
  27. package/dist/mixins/async.d.ts +15 -10
  28. package/dist/mixins/async.js +3 -1
  29. package/dist/stats.js +30 -5
  30. package/dist/utils.d.ts +5 -7
  31. package/dist/utils.js +11 -20
  32. package/package.json +1 -1
  33. package/src/backends/fetch.ts +1 -1
  34. package/src/backends/memory.ts +1 -2
  35. package/src/backends/overlay.ts +2 -2
  36. package/src/backends/store/fs.ts +187 -220
  37. package/src/backends/store/simple.ts +36 -37
  38. package/src/backends/store/store.ts +25 -26
  39. package/src/config.ts +11 -1
  40. package/src/devices.ts +2 -3
  41. package/src/emulation/cache.ts +68 -60
  42. package/src/emulation/index.ts +1 -1
  43. package/src/emulation/promises.ts +20 -19
  44. package/src/emulation/shared.ts +13 -1
  45. package/src/emulation/sync.ts +16 -15
  46. package/src/file.ts +9 -21
  47. package/src/inode.ts +10 -31
  48. package/src/mixins/async.ts +27 -24
  49. package/src/stats.ts +47 -5
  50. package/src/utils.ts +11 -23
  51. package/tests/fs/dir.test.ts +21 -31
  52. package/tests/fs/directory.test.ts +6 -4
  53. package/tests/fs/links.test.ts +9 -2
  54. package/tests/fs/permissions.test.ts +2 -2
  55. package/tests/fs/stat.test.ts +42 -0
  56. package/tests/fs/times.test.ts +28 -28
  57. package/tests/setup/cow+fetch.ts +4 -2
@@ -1,14 +1,15 @@
1
1
  import { credentials } from '../../credentials.js';
2
- import { S_IFDIR, S_IFREG } from '../../emulation/constants.js';
3
- import { basename, dirname, join, resolve } from '../../emulation/path.js';
2
+ import { S_IFDIR, S_IFREG, S_ISGID, S_ISUID } from '../../emulation/constants.js';
3
+ import { basename, dirname, parse, resolve } from '../../emulation/path.js';
4
4
  import { Errno, ErrnoError } from '../../error.js';
5
+ import type { File } from '../../file.js';
5
6
  import { PreloadFile } from '../../file.js';
6
7
  import { FileSystem, type FileSystemMetadata } from '../../filesystem.js';
7
- import { type Ino, Inode, randomIno, rootIno } from '../../inode.js';
8
+ import { Inode, rootIno } from '../../inode.js';
8
9
  import type { FileType, Stats } from '../../stats.js';
9
- import { decodeDirListing, encodeUTF8, encodeDirListing } from '../../utils.js';
10
+ import { decodeDirListing, encodeDirListing, encodeUTF8, randomBigInt } from '../../utils.js';
10
11
  import type { Store, Transaction } from './store.js';
11
- import type { File } from '../../file.js';
12
+ import { serialize } from 'utilium';
12
13
 
13
14
  const maxInodeAllocTries = 5;
14
15
 
@@ -67,182 +68,174 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
67
68
  */
68
69
  public async rename(oldPath: string, newPath: string): Promise<void> {
69
70
  await using tx = this.store.transaction();
70
- const oldParent = dirname(oldPath),
71
- oldName = basename(oldPath),
72
- newParent = dirname(newPath),
73
- newName = basename(newPath),
71
+ const _old = parse(oldPath),
72
+ _new = parse(newPath),
74
73
  // Remove oldPath from parent's directory listing.
75
- oldDirNode = await this.findINode(tx, oldParent),
76
- oldDirList = await this.getDirListing(tx, oldDirNode, oldParent);
74
+ oldDirNode = await this.findInode(tx, _old.dir, 'rename'),
75
+ oldDirList = decodeDirListing(await this.get(tx, oldDirNode.data, _old.dir, 'rename'));
77
76
 
78
- if (!oldDirList[oldName]) {
77
+ if (!oldDirList[_old.base]) {
79
78
  throw ErrnoError.With('ENOENT', oldPath, 'rename');
80
79
  }
81
- const nodeId: Ino = oldDirList[oldName];
82
- delete oldDirList[oldName];
80
+ const ino: bigint = oldDirList[_old.base];
81
+ delete oldDirList[_old.base];
83
82
 
84
83
  /*
85
84
  Can't move a folder inside itself.
86
- This ensures that the check passes only if `oldPath` is a subpath of `newParent`.
85
+ This ensures that the check passes only if `oldPath` is a subpath of `_new.dir`.
87
86
  We append '/' to avoid matching folders that are a substring of the bottom-most folder in the path.
88
87
  */
89
- if ((newParent + '/').indexOf(oldPath + '/') === 0) {
90
- throw new ErrnoError(Errno.EBUSY, oldParent);
88
+ if ((_new.dir + '/').indexOf(oldPath + '/') === 0) {
89
+ throw new ErrnoError(Errno.EBUSY, _old.dir);
91
90
  }
92
91
 
93
92
  // Add newPath to parent's directory listing.
94
93
 
95
- const sameParent = newParent === oldParent;
94
+ const sameParent = _new.dir == _old.dir;
96
95
 
97
- // Prevent us from re-grabbing the same directory listing, which still contains `oldName.`
98
- const newDirNode: Inode = sameParent ? oldDirNode : await this.findINode(tx, newParent);
99
- const newDirList: typeof oldDirList = sameParent ? oldDirList : await this.getDirListing(tx, newDirNode, newParent);
96
+ // Prevent us from re-grabbing the same directory listing, which still contains `old_path.base.`
97
+ const newDirNode: Inode = sameParent ? oldDirNode : await this.findInode(tx, _new.dir, 'rename');
98
+ const newDirList: typeof oldDirList = sameParent ? oldDirList : decodeDirListing(await this.get(tx, newDirNode.data, _new.dir, 'rename'));
100
99
 
101
- if (newDirList[newName]) {
100
+ if (newDirList[_new.base]) {
102
101
  // If it's a file, delete it, if it's a directory, throw a permissions error.
103
- const newNameNode = await this.getINode(tx, newDirList[newName], newPath);
104
- if (!newNameNode.toStats().isFile()) {
102
+ const existing = new Inode(await this.get(tx, newDirList[_new.base], newPath, 'rename'));
103
+ if (!existing.toStats().isFile()) {
105
104
  throw ErrnoError.With('EPERM', newPath, 'rename');
106
105
  }
107
- await tx.remove(newNameNode.ino);
108
- await tx.remove(newDirList[newName]);
106
+ await tx.remove(existing.data);
107
+ await tx.remove(newDirList[_new.base]);
109
108
  }
110
- newDirList[newName] = nodeId;
109
+ newDirList[_new.base] = ino;
111
110
  // Commit the two changed directory listings.
112
- await tx.set(oldDirNode.ino, encodeDirListing(oldDirList));
113
- await tx.set(newDirNode.ino, encodeDirListing(newDirList));
111
+ await tx.set(oldDirNode.data, encodeDirListing(oldDirList));
112
+ await tx.set(newDirNode.data, encodeDirListing(newDirList));
114
113
  await tx.commit();
115
114
  }
116
115
 
117
116
  public renameSync(oldPath: string, newPath: string): void {
118
117
  using tx = this.store.transaction();
119
- const oldParent = dirname(oldPath),
120
- oldName = basename(oldPath),
121
- newParent = dirname(newPath),
122
- newName = basename(newPath),
118
+ const _old = parse(oldPath),
119
+ _new = parse(newPath),
123
120
  // Remove oldPath from parent's directory listing.
124
- oldDirNode = this.findINodeSync(tx, oldParent),
125
- oldDirList = this.getDirListingSync(tx, oldDirNode, oldParent);
121
+ oldDirNode = this.findInodeSync(tx, _old.dir, 'rename'),
122
+ oldDirList = decodeDirListing(this.getSync(tx, oldDirNode.data, _old.dir, 'rename'));
126
123
 
127
- if (!oldDirList[oldName]) {
124
+ if (!oldDirList[_old.base]) {
128
125
  throw ErrnoError.With('ENOENT', oldPath, 'rename');
129
126
  }
130
- const ino: Ino = oldDirList[oldName];
131
- delete oldDirList[oldName];
127
+ const ino: bigint = oldDirList[_old.base];
128
+ delete oldDirList[_old.base];
132
129
 
133
130
  /*
134
131
  Can't move a folder inside itself.
135
- This ensures that the check passes only if `oldPath` is a subpath of `newParent`.
132
+ This ensures that the check passes only if `oldPath` is a subpath of `_new.dir`.
136
133
  We append '/' to avoid matching folders that are a substring of the bottom-most folder in the path.
137
134
  */
138
- if ((newParent + '/').indexOf(oldPath + '/') == 0) {
139
- throw new ErrnoError(Errno.EBUSY, oldParent);
135
+ if ((_new.dir + '/').indexOf(oldPath + '/') == 0) {
136
+ throw new ErrnoError(Errno.EBUSY, _old.dir);
140
137
  }
141
138
 
142
139
  // Add newPath to parent's directory listing.
143
- const sameParent = newParent === oldParent;
140
+ const sameParent = _new.dir === _old.dir;
144
141
 
145
- // Prevent us from re-grabbing the same directory listing, which still contains `oldName.`
146
- const newDirNode: Inode = sameParent ? oldDirNode : this.findINodeSync(tx, newParent);
147
- const newDirList: typeof oldDirList = sameParent ? oldDirList : this.getDirListingSync(tx, newDirNode, newParent);
142
+ // Prevent us from re-grabbing the same directory listing, which still contains `old_path.base.`
143
+ const newDirNode: Inode = sameParent ? oldDirNode : this.findInodeSync(tx, _new.dir, 'rename');
144
+ const newDirList: typeof oldDirList = sameParent ? oldDirList : decodeDirListing(this.getSync(tx, newDirNode.data, _new.dir, 'rename'));
148
145
 
149
- if (newDirList[newName]) {
146
+ if (newDirList[_new.base]) {
150
147
  // If it's a file, delete it, if it's a directory, throw a permissions error.
151
- const newNameNode = this.getINodeSync(tx, newDirList[newName], newPath);
152
- if (!newNameNode.toStats().isFile()) {
148
+ const existing = new Inode(this.getSync(tx, newDirList[_new.base], newPath, 'rename'));
149
+ if (!existing.toStats().isFile()) {
153
150
  throw ErrnoError.With('EPERM', newPath, 'rename');
154
151
  }
155
- tx.removeSync(newNameNode.ino);
156
- tx.removeSync(newDirList[newName]);
152
+ tx.removeSync(existing.data);
153
+ tx.removeSync(newDirList[_new.base]);
157
154
  }
158
- newDirList[newName] = ino;
155
+ newDirList[_new.base] = ino;
159
156
 
160
157
  // Commit the two changed directory listings.
161
- tx.setSync(oldDirNode.ino, encodeDirListing(oldDirList));
162
- tx.setSync(newDirNode.ino, encodeDirListing(newDirList));
158
+ tx.setSync(oldDirNode.data, encodeDirListing(oldDirList));
159
+ tx.setSync(newDirNode.data, encodeDirListing(newDirList));
163
160
  tx.commitSync();
164
161
  }
165
162
 
166
163
  public async stat(path: string): Promise<Stats> {
167
164
  await using tx = this.store.transaction();
168
- return (await this.findINode(tx, path)).toStats();
165
+ return (await this.findInode(tx, path, 'stat')).toStats();
169
166
  }
170
167
 
171
168
  public statSync(path: string): Stats {
172
169
  using tx = this.store.transaction();
173
- return this.findINodeSync(tx, path).toStats();
170
+ return this.findInodeSync(tx, path, 'stat').toStats();
174
171
  }
175
172
 
176
173
  public async createFile(path: string, flag: string, mode: number): Promise<File> {
177
- const node = await this.commitNew(path, S_IFREG, mode, new Uint8Array(0));
178
- return new PreloadFile(this, path, flag, node.toStats(), new Uint8Array(0));
174
+ const node = await this.commitNew(path, S_IFREG, mode, new Uint8Array(), 'createFile');
175
+ return new PreloadFile(this, path, flag, node.toStats(), new Uint8Array());
179
176
  }
180
177
 
181
178
  public createFileSync(path: string, flag: string, mode: number): File {
182
- this.commitNewSync(path, S_IFREG, mode);
183
- return this.openFileSync(path, flag);
179
+ const node = this.commitNewSync(path, S_IFREG, mode, new Uint8Array(), 'createFile');
180
+ return new PreloadFile(this, path, flag, node.toStats(), new Uint8Array());
184
181
  }
185
182
 
186
183
  public async openFile(path: string, flag: string): Promise<File> {
187
184
  await using tx = this.store.transaction();
188
- const node = await this.findINode(tx, path),
189
- data = await tx.get(node.ino);
190
- if (!data) {
191
- throw ErrnoError.With('ENOENT', path, 'openFile');
192
- }
185
+ const node = await this.findInode(tx, path, 'openFile');
186
+ const data = await this.get(tx, node.data, path, 'openFile');
187
+
193
188
  return new PreloadFile(this, path, flag, node.toStats(), data);
194
189
  }
195
190
 
196
191
  public openFileSync(path: string, flag: string): File {
197
192
  using tx = this.store.transaction();
198
- const node = this.findINodeSync(tx, path),
199
- data = tx.getSync(node.ino);
200
- if (!data) {
201
- throw ErrnoError.With('ENOENT', path, 'openFile');
202
- }
193
+ const node = this.findInodeSync(tx, path, 'openFile');
194
+ const data = this.getSync(tx, node.data, path, 'openFile');
195
+
203
196
  return new PreloadFile(this, path, flag, node.toStats(), data);
204
197
  }
205
198
 
206
199
  public async unlink(path: string): Promise<void> {
207
- return this.remove(path, false);
200
+ return this.remove(path, false, 'unlink');
208
201
  }
209
202
 
210
203
  public unlinkSync(path: string): void {
211
- this.removeSync(path, false);
204
+ this.removeSync(path, false, 'unlink');
212
205
  }
213
206
 
214
207
  public async rmdir(path: string): Promise<void> {
215
208
  if ((await this.readdir(path)).length) {
216
209
  throw ErrnoError.With('ENOTEMPTY', path, 'rmdir');
217
210
  }
218
- await this.remove(path, true);
211
+ await this.remove(path, true, 'rmdir');
219
212
  }
220
213
 
221
214
  public rmdirSync(path: string): void {
222
215
  if (this.readdirSync(path).length) {
223
216
  throw ErrnoError.With('ENOTEMPTY', path, 'rmdir');
224
217
  }
225
- this.removeSync(path, true);
218
+ this.removeSync(path, true, 'rmdir');
226
219
  }
227
220
 
228
221
  public async mkdir(path: string, mode: number): Promise<void> {
229
- await this.commitNew(path, S_IFDIR, mode, encodeUTF8('{}'));
222
+ await this.commitNew(path, S_IFDIR, mode, encodeUTF8('{}'), 'mkdir');
230
223
  }
231
224
 
232
225
  public mkdirSync(path: string, mode: number): void {
233
- this.commitNewSync(path, S_IFDIR, mode, encodeUTF8('{}'));
226
+ this.commitNewSync(path, S_IFDIR, mode, encodeUTF8('{}'), 'mkdir');
234
227
  }
235
228
 
236
229
  public async readdir(path: string): Promise<string[]> {
237
230
  await using tx = this.store.transaction();
238
- const node = await this.findINode(tx, path);
239
- return Object.keys(await this.getDirListing(tx, node, path));
231
+ const node = await this.findInode(tx, path, 'readdir');
232
+ return Object.keys(decodeDirListing(await this.get(tx, node.data, path, 'readdir')));
240
233
  }
241
234
 
242
235
  public readdirSync(path: string): string[] {
243
236
  using tx = this.store.transaction();
244
- const node = this.findINodeSync(tx, path);
245
- return Object.keys(this.getDirListingSync(tx, node, path));
237
+ const node = this.findInodeSync(tx, path, 'readdir');
238
+ return Object.keys(decodeDirListing(this.getSync(tx, node.data, path, 'readdir')));
246
239
  }
247
240
 
248
241
  /**
@@ -252,15 +245,15 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
252
245
  public async sync(path: string, data: Uint8Array, stats: Readonly<Stats>): Promise<void> {
253
246
  await using tx = this.store.transaction();
254
247
  // We use _findInode because we actually need the INode id.
255
- const fileInodeId = await this._findINode(tx, dirname(path), basename(path)),
256
- fileInode = await this.getINode(tx, fileInodeId, path),
248
+ const fileInodeId = await this._findInode(tx, path, 'sync'),
249
+ fileInode = new Inode(await this.get(tx, fileInodeId, path, 'sync')),
257
250
  inodeChanged = fileInode.update(stats);
258
251
 
259
252
  // Sync data.
260
- await tx.set(fileInode.ino, data);
253
+ await tx.set(fileInode.data, data);
261
254
  // Sync metadata.
262
255
  if (inodeChanged) {
263
- await tx.set(fileInodeId, fileInode.data);
256
+ await tx.set(fileInodeId, serialize(fileInode));
264
257
  }
265
258
 
266
259
  await tx.commit();
@@ -273,15 +266,15 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
273
266
  public syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void {
274
267
  using tx = this.store.transaction();
275
268
  // We use _findInode because we actually need the INode id.
276
- const fileInodeId = this._findINodeSync(tx, dirname(path), basename(path)),
277
- fileInode = this.getINodeSync(tx, fileInodeId, path),
269
+ const fileInodeId = this._findInodeSync(tx, path, 'sync'),
270
+ fileInode = new Inode(this.getSync(tx, fileInodeId, path, 'sync')),
278
271
  inodeChanged = fileInode.update(stats);
279
272
 
280
273
  // Sync data.
281
- tx.setSync(fileInode.ino, data);
274
+ tx.setSync(fileInode.data, data);
282
275
  // Sync metadata.
283
276
  if (inodeChanged) {
284
- tx.setSync(fileInodeId, fileInode.data);
277
+ tx.setSync(fileInodeId, serialize(fileInode));
285
278
  }
286
279
 
287
280
  tx.commitSync();
@@ -291,17 +284,17 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
291
284
  await using tx = this.store.transaction();
292
285
 
293
286
  const newDir: string = dirname(link),
294
- newDirNode = await this.findINode(tx, newDir),
295
- listing = await this.getDirListing(tx, newDirNode, newDir);
287
+ newDirNode = await this.findInode(tx, newDir, 'link'),
288
+ listing = decodeDirListing(await this.get(tx, newDirNode.data, newDir, 'link'));
296
289
 
297
- const ino = await this._findINode(tx, dirname(target), basename(target));
298
- const node = await this.getINode(tx, ino, target);
290
+ const ino = await this._findInode(tx, target, 'link');
291
+ const node = new Inode(await this.get(tx, ino, target, 'link'));
299
292
 
300
293
  node.nlink++;
301
294
  listing[basename(link)] = ino;
302
295
 
303
- tx.setSync(ino, node.data);
304
- tx.setSync(newDirNode.ino, encodeDirListing(listing));
296
+ tx.setSync(ino, serialize(node));
297
+ tx.setSync(newDirNode.data, encodeDirListing(listing));
305
298
  tx.commitSync();
306
299
  }
307
300
 
@@ -309,17 +302,17 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
309
302
  using tx = this.store.transaction();
310
303
 
311
304
  const newDir: string = dirname(link),
312
- newDirNode = this.findINodeSync(tx, newDir),
313
- listing = this.getDirListingSync(tx, newDirNode, newDir);
305
+ newDirNode = this.findInodeSync(tx, newDir, 'link'),
306
+ listing = decodeDirListing(this.getSync(tx, newDirNode.data, newDir, 'link'));
314
307
 
315
- const ino = this._findINodeSync(tx, dirname(target), basename(target));
316
- const node = this.getINodeSync(tx, ino, target);
308
+ const ino = this._findInodeSync(tx, target, 'link');
309
+ const node = new Inode(this.getSync(tx, ino, target, 'link'));
317
310
 
318
311
  node.nlink++;
319
312
  listing[basename(link)] = ino;
320
313
 
321
- tx.setSync(ino, node.data);
322
- tx.setSync(newDirNode.ino, encodeDirListing(listing));
314
+ tx.setSync(ino, serialize(node));
315
+ tx.setSync(newDirNode.data, encodeDirListing(listing));
323
316
  tx.commitSync();
324
317
  }
325
318
 
@@ -333,10 +326,11 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
333
326
  }
334
327
  // Create new inode. o777, owned by root:root
335
328
  const inode = new Inode();
329
+ inode.ino = rootIno;
336
330
  inode.mode = 0o777 | S_IFDIR;
337
331
  // If the root doesn't exist, the first random ID shouldn't exist either.
338
- await tx.set(inode.ino, encodeUTF8('{}'));
339
- await tx.set(rootIno, inode.data);
332
+ await tx.set(inode.data, encodeUTF8('{}'));
333
+ await tx.set(rootIno, serialize(inode));
340
334
  await tx.commit();
341
335
  }
342
336
 
@@ -350,10 +344,11 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
350
344
  }
351
345
  // Create new inode, mode o777, owned by root:root
352
346
  const inode = new Inode();
347
+ inode.ino = rootIno;
353
348
  inode.mode = 0o777 | S_IFDIR;
354
349
  // If the root doesn't exist, the first random ID shouldn't exist either.
355
- tx.setSync(inode.ino, encodeUTF8('{}'));
356
- tx.setSync(rootIno, inode.data);
350
+ tx.setSync(inode.data, encodeUTF8('{}'));
351
+ tx.setSync(rootIno, serialize(inode));
357
352
  tx.commitSync();
358
353
  }
359
354
 
@@ -363,23 +358,23 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
363
358
  * @param filename The filename of the inode we are attempting to find, minus
364
359
  * the parent.
365
360
  */
366
- private async _findINode(tx: Transaction, parent: string, filename: string, visited: Set<string> = new Set()): Promise<Ino> {
367
- const currentPath = join(parent, filename);
368
- if (visited.has(currentPath)) {
369
- throw new ErrnoError(Errno.EIO, 'Infinite loop detected while finding inode', currentPath);
361
+ private async _findInode(tx: Transaction, path: string, syscall: string, visited: Set<string> = new Set()): Promise<bigint> {
362
+ if (visited.has(path)) {
363
+ throw new ErrnoError(Errno.EIO, 'Infinite loop detected while finding inode', path);
370
364
  }
371
365
 
372
- visited.add(currentPath);
366
+ visited.add(path);
373
367
 
374
- if (parent == '/' && filename === '') {
368
+ if (path == '/') {
375
369
  return rootIno;
376
370
  }
377
371
 
378
- const inode = parent == '/' ? await this.getINode(tx, rootIno, parent) : await this.findINode(tx, parent, visited);
379
- const dirList = await this.getDirListing(tx, inode, parent);
372
+ const { dir: parent, base: filename } = parse(path);
373
+ const inode = parent == '/' ? new Inode(await this.get(tx, rootIno, parent, syscall)) : await this.findInode(tx, parent, syscall, visited);
374
+ const dirList = decodeDirListing(await this.get(tx, inode.data, parent, syscall));
380
375
 
381
376
  if (!(filename in dirList)) {
382
- throw ErrnoError.With('ENOENT', resolve(parent, filename), '_findINode');
377
+ throw ErrnoError.With('ENOENT', resolve(parent, filename), syscall);
383
378
  }
384
379
 
385
380
  return dirList[filename];
@@ -392,23 +387,23 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
392
387
  * the parent.
393
388
  * @return string The ID of the file's inode in the file system.
394
389
  */
395
- protected _findINodeSync(tx: Transaction, parent: string, filename: string, visited: Set<string> = new Set()): Ino {
396
- const currentPath = join(parent, filename);
397
- if (visited.has(currentPath)) {
398
- throw new ErrnoError(Errno.EIO, 'Infinite loop detected while finding inode', currentPath);
390
+ protected _findInodeSync(tx: Transaction, path: string, syscall: string, visited: Set<string> = new Set()): bigint {
391
+ if (visited.has(path)) {
392
+ throw new ErrnoError(Errno.EIO, 'Infinite loop detected while finding inode', path);
399
393
  }
400
394
 
401
- visited.add(currentPath);
395
+ visited.add(path);
402
396
 
403
- if (parent == '/' && filename === '') {
397
+ if (path == '/') {
404
398
  return rootIno;
405
399
  }
406
400
 
407
- const inode = parent == '/' ? this.getINodeSync(tx, rootIno, parent) : this.findINodeSync(tx, parent, visited);
408
- const dir = this.getDirListingSync(tx, inode, parent);
401
+ const { dir: parent, base: filename } = parse(path);
402
+ const inode = parent == '/' ? new Inode(this.getSync(tx, rootIno, parent, syscall)) : this.findInodeSync(tx, parent, syscall, visited);
403
+ const dir = decodeDirListing(this.getSync(tx, inode.data, parent, syscall));
409
404
 
410
405
  if (!(filename in dir)) {
411
- throw ErrnoError.With('ENOENT', resolve(parent, filename), '_findINode');
406
+ throw ErrnoError.With('ENOENT', resolve(parent, filename), syscall);
412
407
  }
413
408
 
414
409
  return dir[filename];
@@ -419,9 +414,9 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
419
414
  * @param path The path to look up.
420
415
  * @todo memoize/cache
421
416
  */
422
- private async findINode(tx: Transaction, path: string, visited: Set<string> = new Set()): Promise<Inode> {
423
- const id = await this._findINode(tx, dirname(path), basename(path), visited);
424
- return this.getINode(tx, id, path);
417
+ private async findInode(tx: Transaction, path: string, syscall: string, visited: Set<string> = new Set()): Promise<Inode> {
418
+ const ino = await this._findInode(tx, path, syscall, visited);
419
+ return new Inode(await this.get(tx, ino, path, syscall));
425
420
  }
426
421
 
427
422
  /**
@@ -430,99 +425,68 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
430
425
  * @return The Inode of the path p.
431
426
  * @todo memoize/cache
432
427
  */
433
- protected findINodeSync(tx: Transaction, path: string, visited: Set<string> = new Set()): Inode {
434
- const ino = this._findINodeSync(tx, dirname(path), basename(path), visited);
435
- return this.getINodeSync(tx, ino, path);
428
+ protected findInodeSync(tx: Transaction, path: string, syscall: string, visited: Set<string> = new Set()): Inode {
429
+ const ino = this._findInodeSync(tx, path, syscall, visited);
430
+ return new Inode(this.getSync(tx, ino, path, syscall));
436
431
  }
437
432
 
438
433
  /**
439
- * Given the ID of a node, retrieves the corresponding Inode.
434
+ * Given an ID, retrieves the corresponding data.
440
435
  * @param tx The transaction to use.
441
436
  * @param path The corresponding path to the file (used for error messages).
442
437
  * @param id The ID to look up.
443
438
  */
444
- private async getINode(tx: Transaction, id: Ino, path: string): Promise<Inode> {
439
+ private async get(tx: Transaction, id: bigint, path: string, syscall: string): Promise<Uint8Array> {
445
440
  const data = await tx.get(id);
446
441
  if (!data) {
447
- throw ErrnoError.With('ENOENT', path, 'getINode');
442
+ throw ErrnoError.With('ENOENT', path, syscall);
448
443
  }
449
- return new Inode(data.buffer);
444
+ return data;
450
445
  }
451
446
 
452
447
  /**
453
- * Given the ID of a node, retrieves the corresponding Inode.
448
+ * Given an ID, retrieves the corresponding data.
454
449
  * @param tx The transaction to use.
455
450
  * @param path The corresponding path to the file (used for error messages).
456
451
  * @param id The ID to look up.
457
452
  */
458
- protected getINodeSync(tx: Transaction, id: Ino, path: string): Inode {
453
+ private getSync(tx: Transaction, id: bigint, path: string, syscall: string): Uint8Array {
459
454
  const data = tx.getSync(id);
460
455
  if (!data) {
461
- throw ErrnoError.With('ENOENT', path, 'getINode');
456
+ throw ErrnoError.With('ENOENT', path, syscall);
462
457
  }
463
- const inode = new Inode(data.buffer);
464
- return inode;
465
- }
466
-
467
- /**
468
- * Given the Inode of a directory, retrieves the corresponding directory
469
- * listing.
470
- */
471
- private async getDirListing(tx: Transaction, inode: Inode, path: string): Promise<{ [fileName: string]: Ino }> {
472
- const data = await tx.get(inode.ino);
473
- /*
474
- Occurs when data is undefined,or corresponds to something other than a directory listing.
475
- The latter should never occur unless the file system is corrupted.
476
- */
477
- if (!data) {
478
- throw ErrnoError.With('ENOENT', path, 'getDirListing');
479
- }
480
-
481
- return decodeDirListing(data);
482
- }
483
-
484
- /**
485
- * Given the Inode of a directory, retrieves the corresponding directory listing.
486
- */
487
- protected getDirListingSync(tx: Transaction, inode: Inode, p?: string): { [fileName: string]: Ino } {
488
- const data = tx.getSync(inode.ino);
489
- if (!data) {
490
- throw ErrnoError.With('ENOENT', p, 'getDirListing');
491
- }
492
- return decodeDirListing(data);
458
+ return data;
493
459
  }
494
460
 
495
461
  /**
496
462
  * Adds a new node under a random ID. Retries before giving up in
497
- * the exceedingly unlikely chance that we try to reuse a random ino.
463
+ * the exceedingly unlikely chance that we try to reuse a random id.
498
464
  */
499
- private async addNew(tx: Transaction, data: Uint8Array, path: string): Promise<Ino> {
465
+ private async allocNew(tx: Transaction, path: string, syscall: string): Promise<bigint> {
500
466
  for (let i = 0; i < maxInodeAllocTries; i++) {
501
- const ino: Ino = randomIno();
467
+ const ino: bigint = randomBigInt();
502
468
  if (await tx.get(ino)) {
503
469
  continue;
504
470
  }
505
- await tx.set(ino, data);
506
471
  return ino;
507
472
  }
508
- throw new ErrnoError(Errno.ENOSPC, 'No inode IDs available', path, 'addNewNode');
473
+ throw new ErrnoError(Errno.ENOSPC, 'No IDs available', path, syscall);
509
474
  }
510
475
 
511
476
  /**
512
477
  * Creates a new node under a random ID. Retries before giving up in
513
- * the exceedingly unlikely chance that we try to reuse a random ino.
478
+ * the exceedingly unlikely chance that we try to reuse a random id.
514
479
  * @return The ino that the data was stored under.
515
480
  */
516
- protected addNewSync(tx: Transaction, data: Uint8Array, path: string): Ino {
481
+ private allocNewSync(tx: Transaction, path: string, syscall: string): bigint {
517
482
  for (let i = 0; i < maxInodeAllocTries; i++) {
518
- const ino: Ino = randomIno();
483
+ const ino: bigint = randomBigInt();
519
484
  if (tx.getSync(ino)) {
520
485
  continue;
521
486
  }
522
- tx.setSync(ino, data);
523
487
  return ino;
524
488
  }
525
- throw new ErrnoError(Errno.ENOSPC, 'No inode IDs available', path, 'addNewNode');
489
+ throw new ErrnoError(Errno.ENOSPC, 'No IDs available', path, syscall);
526
490
  }
527
491
 
528
492
  /**
@@ -533,40 +497,41 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
533
497
  * @param mode The mode to create the new file with.
534
498
  * @param data The data to store at the file's data node.
535
499
  */
536
- private async commitNew(path: string, type: FileType, mode: number, data: Uint8Array): Promise<Inode> {
500
+ private async commitNew(path: string, type: FileType, mode: number, data: Uint8Array, syscall: string): Promise<Inode> {
537
501
  /*
538
502
  The root always exists.
539
503
  If we don't check this prior to taking steps below,
540
504
  we will create a file with name '' in root if path is '/'.
541
505
  */
542
506
  if (path == '/') {
543
- throw ErrnoError.With('EEXIST', path, 'commitNew');
507
+ throw ErrnoError.With('EEXIST', path, syscall);
544
508
  }
545
509
 
546
510
  await using tx = this.store.transaction();
547
- const parentPath = dirname(path),
548
- parent = await this.findINode(tx, parentPath);
549
511
 
550
- const fname = basename(path),
551
- listing = await this.getDirListing(tx, parent, parentPath);
512
+ const { dir: parentPath, base: fname } = parse(path);
513
+ const parent = await this.findInode(tx, parentPath, syscall);
514
+ const listing = decodeDirListing(await this.get(tx, parent.data, parentPath, syscall));
552
515
 
553
516
  // Check if file already exists.
554
517
  if (listing[fname]) {
555
- await tx.abort();
556
- throw ErrnoError.With('EEXIST', path, 'commitNew');
518
+ throw ErrnoError.With('EEXIST', path, syscall);
557
519
  }
558
520
 
559
521
  // Commit data.
560
522
  const inode = new Inode();
561
- inode.ino = await this.addNew(tx, data, path);
523
+ inode.ino = await this.allocNew(tx, path, syscall);
524
+ inode.data = await this.allocNew(tx, path, syscall);
562
525
  inode.mode = mode | type;
563
- inode.uid = credentials.uid;
564
- inode.gid = credentials.gid;
526
+ inode.uid = parent.mode & S_ISUID ? parent.uid : credentials.uid;
527
+ inode.gid = parent.mode & S_ISGID ? parent.gid : credentials.gid;
565
528
  inode.size = data.length;
529
+ await tx.set(inode.ino, serialize(inode));
530
+ await tx.set(inode.data, data);
566
531
 
567
532
  // Update and commit parent directory listing.
568
- listing[fname] = await this.addNew(tx, inode.data, path);
569
- await tx.set(parent.ino, encodeDirListing(listing));
533
+ listing[fname] = inode.ino;
534
+ await tx.set(parent.data, encodeDirListing(listing));
570
535
  await tx.commit();
571
536
  return inode;
572
537
  }
@@ -580,40 +545,43 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
580
545
  * @param data The data to store at the file's data node.
581
546
  * @return The Inode for the new file.
582
547
  */
583
- protected commitNewSync(path: string, type: FileType, mode: number, data: Uint8Array = new Uint8Array()): Inode {
548
+ private commitNewSync(path: string, type: FileType, mode: number, data: Uint8Array, syscall: string): Inode {
584
549
  /*
585
550
  The root always exists.
586
551
  If we don't check this prior to taking steps below,
587
552
  we will create a file with name '' in root if path is '/'.
588
553
  */
589
554
  if (path == '/') {
590
- throw ErrnoError.With('EEXIST', path, 'commitNew');
555
+ throw ErrnoError.With('EEXIST', path, syscall);
591
556
  }
592
557
 
593
558
  using tx = this.store.transaction();
594
- const parentPath = dirname(path),
595
- parent = this.findINodeSync(tx, parentPath);
596
559
 
597
- const fname = basename(path),
598
- listing = this.getDirListingSync(tx, parent, parentPath);
560
+ const { dir: parentPath, base: fname } = parse(path);
561
+ const parent = this.findInodeSync(tx, parentPath, syscall);
562
+
563
+ const listing = decodeDirListing(this.getSync(tx, parent.data, parentPath, syscall));
599
564
 
600
565
  // Check if file already exists.
601
566
  if (listing[fname]) {
602
- throw ErrnoError.With('EEXIST', path, 'commitNew');
567
+ throw ErrnoError.With('EEXIST', path, syscall);
603
568
  }
604
569
 
605
570
  // Commit data.
606
- const node = new Inode();
607
- node.ino = this.addNewSync(tx, data, path);
608
- node.size = data.length;
609
- node.mode = mode | type;
610
- node.uid = credentials.uid;
611
- node.gid = credentials.gid;
571
+ const inode = new Inode();
572
+ inode.ino = this.allocNewSync(tx, path, syscall);
573
+ inode.data = this.allocNewSync(tx, path, syscall);
574
+ inode.size = data.length;
575
+ inode.mode = mode | type;
576
+ inode.uid = parent.mode & S_ISUID ? parent.uid : credentials.uid;
577
+ inode.gid = parent.mode & S_ISGID ? parent.gid : credentials.gid;
612
578
  // Update and commit parent directory listing.
613
- listing[fname] = this.addNewSync(tx, node.data, path);
614
- tx.setSync(parent.ino, encodeDirListing(listing));
579
+ tx.setSync(inode.ino, serialize(inode));
580
+ tx.setSync(inode.data, data);
581
+ listing[fname] = inode.ino;
582
+ tx.setSync(parent.data, encodeDirListing(listing));
615
583
  tx.commitSync();
616
- return node;
584
+ return inode;
617
585
  }
618
586
 
619
587
  /**
@@ -622,12 +590,12 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
622
590
  * @param isDir Does the path belong to a directory, or a file?
623
591
  * @todo Update mtime.
624
592
  */
625
- private async remove(path: string, isDir: boolean): Promise<void> {
593
+ private async remove(path: string, isDir: boolean, syscall: string): Promise<void> {
626
594
  await using tx = this.store.transaction();
627
- const parent: string = dirname(path),
628
- parentNode = await this.findINode(tx, parent),
629
- listing = await this.getDirListing(tx, parentNode, parent),
630
- fileName: string = basename(path);
595
+
596
+ const { dir: parent, base: fileName } = parse(path),
597
+ parentNode = await this.findInode(tx, parent, syscall),
598
+ listing = decodeDirListing(await this.get(tx, parentNode.data, parent, syscall));
631
599
 
632
600
  if (!listing[fileName]) {
633
601
  throw ErrnoError.With('ENOENT', path, 'remove');
@@ -636,7 +604,7 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
636
604
  const fileIno = listing[fileName];
637
605
 
638
606
  // Get file inode.
639
- const fileNode = await this.getINode(tx, fileIno, path);
607
+ const fileNode = new Inode(await this.get(tx, fileIno, path, syscall));
640
608
 
641
609
  // Remove from directory listing of parent.
642
610
  delete listing[fileName];
@@ -645,11 +613,11 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
645
613
  throw ErrnoError.With('EISDIR', path, 'remove');
646
614
  }
647
615
 
648
- await tx.set(parentNode.ino, encodeDirListing(listing));
616
+ await tx.set(parentNode.data, encodeDirListing(listing));
649
617
 
650
618
  if (--fileNode.nlink < 1) {
651
619
  // remove file
652
- await tx.remove(fileNode.ino);
620
+ await tx.remove(fileNode.data);
653
621
  await tx.remove(fileIno);
654
622
  }
655
623
 
@@ -663,20 +631,19 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
663
631
  * @param isDir Does the path belong to a directory, or a file?
664
632
  * @todo Update mtime.
665
633
  */
666
- protected removeSync(path: string, isDir: boolean): void {
634
+ private removeSync(path: string, isDir: boolean, syscall: string): void {
667
635
  using tx = this.store.transaction();
668
- const parent: string = dirname(path),
669
- parentNode = this.findINodeSync(tx, parent),
670
- listing = this.getDirListingSync(tx, parentNode, parent),
671
- fileName: string = basename(path),
672
- fileIno: Ino = listing[fileName];
636
+ const { dir: parent, base: fileName } = parse(path),
637
+ parentNode = this.findInodeSync(tx, parent, syscall),
638
+ listing = decodeDirListing(this.getSync(tx, parentNode.data, parent, syscall)),
639
+ fileIno: bigint = listing[fileName];
673
640
 
674
641
  if (!fileIno) {
675
642
  throw ErrnoError.With('ENOENT', path, 'remove');
676
643
  }
677
644
 
678
645
  // Get file inode.
679
- const fileNode = this.getINodeSync(tx, fileIno, path);
646
+ const fileNode = new Inode(this.getSync(tx, fileIno, path, syscall));
680
647
 
681
648
  // Remove from directory listing of parent.
682
649
  delete listing[fileName];
@@ -686,11 +653,11 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
686
653
  }
687
654
 
688
655
  // Update directory listing.
689
- tx.setSync(parentNode.ino, encodeDirListing(listing));
656
+ tx.setSync(parentNode.data, encodeDirListing(listing));
690
657
 
691
658
  if (--fileNode.nlink < 1) {
692
659
  // remove file
693
- tx.removeSync(fileNode.ino);
660
+ tx.removeSync(fileNode.data);
694
661
  tx.removeSync(fileIno);
695
662
  }
696
663