@zenfs/core 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/inode.ts ADDED
@@ -0,0 +1,229 @@
1
+ import { Stats, type StatsLike } from './stats.js';
2
+
3
+ /**
4
+ * Alias for an ino.
5
+ * This will be helpful if in the future inode numbers/IDs are changed to strings or numbers.
6
+ */
7
+ export type Ino = bigint;
8
+
9
+ /**
10
+ * Max 32-bit integer
11
+ * @hidden
12
+ */
13
+ export const size_max = 2 ** 32 - 1;
14
+
15
+ /**
16
+ * Room inode
17
+ * @hidden
18
+ */
19
+ export const rootIno: Ino = 0n;
20
+
21
+ /**
22
+ * Generates a random 32 bit integer, then converts to a hex string
23
+ */
24
+ function _random() {
25
+ return Math.round(Math.random() * 2 ** 32).toString(16);
26
+ }
27
+
28
+ /**
29
+ * Generate a random ino
30
+ * @internal
31
+ */
32
+ export function randomIno(): Ino {
33
+ return BigInt('0x' + _random() + _random());
34
+ }
35
+
36
+ /**
37
+ * Offsets for inode members
38
+ */
39
+ enum Offset {
40
+ ino = 0,
41
+ size = 8, // offsets with a 64-bit size
42
+ mode = 12, // 16
43
+ nlink = 14, // 18
44
+ uid = 18, // 22
45
+ gid = 22, // 26
46
+ atime = 26, // 30
47
+ birthtime = 34, // 38
48
+ mtime = 42, // 46
49
+ ctime = 50, // 54
50
+ end = 58, // 62
51
+ }
52
+
53
+ /**
54
+ * Generic inode definition that can easily be serialized.
55
+ */
56
+ export class Inode implements StatsLike {
57
+ public readonly buffer: ArrayBufferLike;
58
+
59
+ public get data(): Uint8Array {
60
+ return new Uint8Array(this.buffer);
61
+ }
62
+
63
+ protected view: DataView;
64
+
65
+ constructor(buffer?: ArrayBufferLike) {
66
+ const setDefaults = !buffer;
67
+ buffer ??= new ArrayBuffer(Offset.end);
68
+ if (buffer?.byteLength < Offset.end) {
69
+ throw new RangeError(`Can not create an inode from a buffer less than ${Offset.end} bytes`);
70
+ }
71
+ this.view = new DataView(buffer);
72
+ this.buffer = buffer;
73
+
74
+ if (!setDefaults) {
75
+ return;
76
+ }
77
+
78
+ // set defaults on a fresh inode
79
+ this.ino = randomIno();
80
+ this.nlink = 1;
81
+ this.size = 4096;
82
+ const now = Date.now();
83
+ this.atimeMs = now;
84
+ this.mtimeMs = now;
85
+ this.ctimeMs = now;
86
+ this.birthtimeMs = now;
87
+ }
88
+
89
+ public get ino(): Ino {
90
+ return this.view.getBigUint64(Offset.ino, true);
91
+ }
92
+
93
+ public set ino(value: Ino) {
94
+ this.view.setBigUint64(Offset.ino, value, true);
95
+ }
96
+
97
+ public get size(): number {
98
+ return this.view.getUint32(Offset.size, true);
99
+ }
100
+
101
+ public set size(value: number) {
102
+ this.view.setUint32(Offset.size, value, true);
103
+ }
104
+
105
+ public get mode(): number {
106
+ return this.view.getUint16(Offset.mode, true);
107
+ }
108
+
109
+ public set mode(value: number) {
110
+ this.view.setUint16(Offset.mode, value, true);
111
+ }
112
+
113
+ public get nlink(): number {
114
+ return this.view.getUint32(Offset.nlink, true);
115
+ }
116
+
117
+ public set nlink(value: number) {
118
+ this.view.setUint32(Offset.nlink, value, true);
119
+ }
120
+
121
+ public get uid(): number {
122
+ return this.view.getUint32(Offset.uid, true);
123
+ }
124
+
125
+ public set uid(value: number) {
126
+ this.view.setUint32(Offset.uid, value, true);
127
+ }
128
+
129
+ public get gid(): number {
130
+ return this.view.getUint32(Offset.gid, true);
131
+ }
132
+
133
+ public set gid(value: number) {
134
+ this.view.setUint32(Offset.gid, value, true);
135
+ }
136
+
137
+ public get atimeMs(): number {
138
+ return this.view.getFloat64(Offset.atime, true);
139
+ }
140
+
141
+ public set atimeMs(value: number) {
142
+ this.view.setFloat64(Offset.atime, value, true);
143
+ }
144
+
145
+ public get birthtimeMs(): number {
146
+ return this.view.getFloat64(Offset.birthtime, true);
147
+ }
148
+
149
+ public set birthtimeMs(value: number) {
150
+ this.view.setFloat64(Offset.birthtime, value, true);
151
+ }
152
+
153
+ public get mtimeMs(): number {
154
+ return this.view.getFloat64(Offset.mtime, true);
155
+ }
156
+
157
+ public set mtimeMs(value: number) {
158
+ this.view.setFloat64(Offset.mtime, value, true);
159
+ }
160
+
161
+ public get ctimeMs(): number {
162
+ return this.view.getFloat64(Offset.ctime, true);
163
+ }
164
+
165
+ public set ctimeMs(value: number) {
166
+ this.view.setFloat64(Offset.ctime, value, true);
167
+ }
168
+
169
+ /**
170
+ * Handy function that converts the Inode to a Node Stats object.
171
+ */
172
+ public toStats(): Stats {
173
+ return new Stats(this);
174
+ }
175
+
176
+ /**
177
+ * Updates the Inode using information from the stats object. Used by file
178
+ * systems at sync time, e.g.:
179
+ * - Program opens file and gets a File object.
180
+ * - Program mutates file. File object is responsible for maintaining
181
+ * metadata changes locally -- typically in a Stats object.
182
+ * - Program closes file. File object's metadata changes are synced with the
183
+ * file system.
184
+ * @return True if any changes have occurred.
185
+ */
186
+ public update(stats: Readonly<Stats>): boolean {
187
+ let hasChanged = false;
188
+ if (this.size !== stats.size) {
189
+ this.size = stats.size;
190
+ hasChanged = true;
191
+ }
192
+
193
+ if (this.mode !== stats.mode) {
194
+ this.mode = stats.mode;
195
+ hasChanged = true;
196
+ }
197
+
198
+ if (this.nlink !== stats.nlink) {
199
+ this.nlink = stats.nlink;
200
+ hasChanged = true;
201
+ }
202
+
203
+ if (this.uid !== stats.uid) {
204
+ this.uid = stats.uid;
205
+ hasChanged = true;
206
+ }
207
+
208
+ if (this.uid !== stats.uid) {
209
+ this.uid = stats.uid;
210
+ hasChanged = true;
211
+ }
212
+
213
+ if (this.atimeMs !== stats.atimeMs) {
214
+ this.atimeMs = stats.atimeMs;
215
+ hasChanged = true;
216
+ }
217
+ if (this.mtimeMs !== stats.mtimeMs) {
218
+ this.mtimeMs = stats.mtimeMs;
219
+ hasChanged = true;
220
+ }
221
+
222
+ if (this.ctimeMs !== stats.ctimeMs) {
223
+ this.ctimeMs = stats.ctimeMs;
224
+ hasChanged = true;
225
+ }
226
+
227
+ return hasChanged;
228
+ }
229
+ }
package/src/mutex.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Non-recursive mutex
3
+ * @internal
4
+ */
5
+ export class Mutex {
6
+ private _locks: Map<string, (() => void)[]> = new Map();
7
+
8
+ public lock(path: string): Promise<void> {
9
+ return new Promise(resolve => {
10
+ if (this._locks.has(path)) {
11
+ this._locks.get(path).push(resolve);
12
+ } else {
13
+ this._locks.set(path, []);
14
+ }
15
+ });
16
+ }
17
+
18
+ public unlock(path: string): void {
19
+ if (!this._locks.has(path)) {
20
+ throw new Error('unlock of a non-locked mutex');
21
+ }
22
+
23
+ const next = this._locks.get(path).shift();
24
+ /*
25
+ don't unlock - we want to queue up next for the
26
+ end of the current task execution, but we don't
27
+ want it to be called inline with whatever the
28
+ current stack is. This way we still get the nice
29
+ behavior that an unlock immediately followed by a
30
+ lock won't cause starvation.
31
+ */
32
+ if (next) {
33
+ setTimeout(next, 0);
34
+ return;
35
+ }
36
+
37
+ this._locks.delete(path);
38
+ }
39
+
40
+ public tryLock(path: string): boolean {
41
+ if (this._locks.has(path)) {
42
+ return false;
43
+ }
44
+
45
+ this._locks.set(path, []);
46
+ return true;
47
+ }
48
+
49
+ public isLocked(path: string): boolean {
50
+ return this._locks.has(path);
51
+ }
52
+ }
package/src/stats.ts ADDED
@@ -0,0 +1,385 @@
1
+ import type * as Node from 'fs';
2
+ import { Cred } from './cred.js';
3
+ import { S_IFDIR, S_IFLNK, S_IFMT, S_IFREG, S_IRWXG, S_IRWXO, S_IRWXU } from './emulation/constants.js';
4
+
5
+ /**
6
+ * Indicates the type of the given file. Applied to 'mode'.
7
+ */
8
+ export enum FileType {
9
+ FILE = S_IFREG,
10
+ DIRECTORY = S_IFDIR,
11
+ SYMLINK = S_IFLNK,
12
+ }
13
+
14
+ /**
15
+ *
16
+ */
17
+ export interface StatsLike {
18
+ /**
19
+ * Size of the item in bytes.
20
+ * For directories/symlinks, this is normally the size of the struct that represents the item.
21
+ */
22
+ size: number | bigint;
23
+ /**
24
+ * Unix-style file mode (e.g. 0o644) that includes the item type
25
+ * Type of the item can be FILE, DIRECTORY, SYMLINK, or SOCKET
26
+ */
27
+ mode: number | bigint;
28
+ /**
29
+ * time of last access, in milliseconds since epoch
30
+ */
31
+ atimeMs: number | bigint;
32
+ /**
33
+ * time of last modification, in milliseconds since epoch
34
+ */
35
+ mtimeMs: number | bigint;
36
+ /**
37
+ * time of last time file status was changed, in milliseconds since epoch
38
+ */
39
+ ctimeMs: number | bigint;
40
+ /**
41
+ * time of file creation, in milliseconds since epoch
42
+ */
43
+ birthtimeMs: number | bigint;
44
+ /**
45
+ * the id of the user that owns the file
46
+ */
47
+ uid: number | bigint;
48
+ /**
49
+ * the id of the group that owns the file
50
+ */
51
+ gid: number | bigint;
52
+ }
53
+
54
+ /**
55
+ * Provides information about a particular entry in the file system.
56
+ * Common code used by both Stats and BigIntStats.
57
+ */
58
+ export abstract class StatsCommon<T extends number | bigint> implements Node.StatsBase<T>, StatsLike {
59
+ protected abstract _isBigint: boolean;
60
+
61
+ protected get _typename(): string {
62
+ return this._isBigint ? 'bigint' : 'number';
63
+ }
64
+
65
+ protected get _typename_inverse(): string {
66
+ return this._isBigint ? 'number' : 'bigint';
67
+ }
68
+
69
+ protected _convert(arg: number | bigint | string | boolean): T {
70
+ return <T>(this._isBigint ? BigInt(arg) : Number(arg));
71
+ }
72
+
73
+ public blocks: T;
74
+
75
+ /**
76
+ * Unix-style file mode (e.g. 0o644) that includes the type of the item.
77
+ * Type of the item can be FILE, DIRECTORY, SYMLINK, or SOCKET
78
+ */
79
+ public mode: T;
80
+
81
+ /**
82
+ * ID of device containing file
83
+ */
84
+ public dev: T = this._convert(0);
85
+
86
+ /**
87
+ * inode number
88
+ */
89
+ public ino: T = this._convert(0);
90
+
91
+ /**
92
+ * device ID (if special file)
93
+ */
94
+ public rdev: T = this._convert(0);
95
+
96
+ /**
97
+ * number of hard links
98
+ */
99
+ public nlink: T = this._convert(1);
100
+
101
+ /**
102
+ * blocksize for file system I/O
103
+ */
104
+ public blksize: T = this._convert(4096);
105
+
106
+ /**
107
+ * user ID of owner
108
+ */
109
+ public uid: T = this._convert(0);
110
+
111
+ /**
112
+ * group ID of owner
113
+ */
114
+ public gid: T = this._convert(0);
115
+
116
+ /**
117
+ * Some file systems stash data on stats objects.
118
+ */
119
+ public fileData?: Uint8Array = null;
120
+
121
+ /**
122
+ * time of last access, in milliseconds since epoch
123
+ */
124
+ public atimeMs: T;
125
+
126
+ public get atime(): Date {
127
+ return new Date(Number(this.atimeMs));
128
+ }
129
+
130
+ public set atime(value: Date) {
131
+ this.atimeMs = this._convert(value.getTime());
132
+ }
133
+
134
+ /**
135
+ * time of last modification, in milliseconds since epoch
136
+ */
137
+ public mtimeMs: T;
138
+
139
+ public get mtime(): Date {
140
+ return new Date(Number(this.mtimeMs));
141
+ }
142
+
143
+ public set mtime(value: Date) {
144
+ this.mtimeMs = this._convert(value.getTime());
145
+ }
146
+
147
+ /**
148
+ * time of last time file status was changed, in milliseconds since epoch
149
+ */
150
+ public ctimeMs: T;
151
+
152
+ public get ctime(): Date {
153
+ return new Date(Number(this.ctimeMs));
154
+ }
155
+
156
+ public set ctime(value: Date) {
157
+ this.ctimeMs = this._convert(value.getTime());
158
+ }
159
+
160
+ /**
161
+ * time of file creation, in milliseconds since epoch
162
+ */
163
+ public birthtimeMs: T;
164
+
165
+ public get birthtime(): Date {
166
+ return new Date(Number(this.birthtimeMs));
167
+ }
168
+
169
+ public set birthtime(value: Date) {
170
+ this.birthtimeMs = this._convert(value.getTime());
171
+ }
172
+
173
+ /**
174
+ * Size of the item in bytes.
175
+ * For directories/symlinks, this is normally the size of the struct that represents the item.
176
+ */
177
+ public size: T;
178
+
179
+ /**
180
+ * Creates a new stats instance from a stats-like object. Can be used to copy stats (note)
181
+ */
182
+ constructor({ atimeMs, mtimeMs, ctimeMs, birthtimeMs, uid, gid, size, mode }: Partial<StatsLike> = {}) {
183
+ const currentTime = Date.now();
184
+ const resolveT = (val: number | bigint, _default: number) => <T>(typeof val == this._typename ? val : this._convert(typeof val == this._typename_inverse ? val : _default));
185
+ this.atimeMs = resolveT(atimeMs, currentTime);
186
+ this.mtimeMs = resolveT(mtimeMs, currentTime);
187
+ this.ctimeMs = resolveT(ctimeMs, currentTime);
188
+ this.birthtimeMs = resolveT(birthtimeMs, currentTime);
189
+ this.uid = resolveT(uid, 0);
190
+ this.gid = resolveT(gid, 0);
191
+ this.size = this._convert(size);
192
+ const itemType: FileType = Number(mode) & S_IFMT || FileType.FILE;
193
+
194
+ if (mode) {
195
+ this.mode = this._convert(mode);
196
+ } else {
197
+ switch (itemType) {
198
+ case FileType.FILE:
199
+ this.mode = this._convert(0o644);
200
+ break;
201
+ case FileType.DIRECTORY:
202
+ default:
203
+ this.mode = this._convert(0o777);
204
+ }
205
+ }
206
+ // number of 512B blocks allocated
207
+ this.blocks = this._convert(Math.ceil(Number(size) / 512));
208
+ // Check if mode also includes top-most bits, which indicate the file's type.
209
+ if ((this.mode & S_IFMT) == 0) {
210
+ this.mode = <T>(this.mode | this._convert(itemType));
211
+ }
212
+ }
213
+
214
+ /**
215
+ * @returns true if this item is a file.
216
+ */
217
+ public isFile(): boolean {
218
+ return (this.mode & S_IFMT) === S_IFREG;
219
+ }
220
+
221
+ /**
222
+ * @returns True if this item is a directory.
223
+ */
224
+ public isDirectory(): boolean {
225
+ return (this.mode & S_IFMT) === S_IFDIR;
226
+ }
227
+
228
+ /**
229
+ * @returns true if this item is a symbolic link
230
+ */
231
+ public isSymbolicLink(): boolean {
232
+ return (this.mode & S_IFMT) === S_IFLNK;
233
+ }
234
+
235
+ // Currently unsupported
236
+
237
+ public isSocket(): boolean {
238
+ return false;
239
+ }
240
+
241
+ public isBlockDevice(): boolean {
242
+ return false;
243
+ }
244
+
245
+ public isCharacterDevice(): boolean {
246
+ return false;
247
+ }
248
+
249
+ public isFIFO(): boolean {
250
+ return false;
251
+ }
252
+
253
+ /**
254
+ * Checks if a given user/group has access to this item
255
+ * @param mode The requested access, combination of W_OK, R_OK, and X_OK
256
+ * @param cred The requesting credentials
257
+ * @returns True if the request has access, false if the request does not
258
+ * @internal
259
+ */
260
+ public hasAccess(mode: number, cred: Cred): boolean {
261
+ if (cred.euid === 0 || cred.egid === 0) {
262
+ //Running as root
263
+ return true;
264
+ }
265
+
266
+ // Mask for
267
+ const adjusted = (cred.uid == this.uid ? S_IRWXU : 0) | (cred.gid == this.gid ? S_IRWXG : 0) | S_IRWXO;
268
+ return (mode & this.mode & adjusted) == mode;
269
+ }
270
+
271
+ /**
272
+ * Convert the current stats object into a credentials object
273
+ * @internal
274
+ */
275
+ public cred(uid: number = Number(this.uid), gid: number = Number(this.gid)): Cred {
276
+ return {
277
+ uid,
278
+ gid,
279
+ suid: Number(this.uid),
280
+ sgid: Number(this.gid),
281
+ euid: uid,
282
+ egid: gid,
283
+ };
284
+ }
285
+
286
+ /**
287
+ * Change the mode of the file. We use this helper function to prevent messing
288
+ * up the type of the file, which is encoded in mode.
289
+ * @internal
290
+ */
291
+ public chmod(mode: number): void {
292
+ this.mode = this._convert((this.mode & S_IFMT) | mode);
293
+ }
294
+
295
+ /**
296
+ * Change the owner user/group of the file.
297
+ * This function makes sure it is a valid UID/GID (that is, a 32 unsigned int)
298
+ * @internal
299
+ */
300
+ public chown(uid: number | bigint, gid: number | bigint): void {
301
+ uid = Number(uid);
302
+ gid = Number(gid);
303
+ if (!isNaN(uid) && 0 <= uid && uid < 2 ** 32) {
304
+ this.uid = this._convert(uid);
305
+ }
306
+ if (!isNaN(gid) && 0 <= gid && gid < 2 ** 32) {
307
+ this.gid = this._convert(gid);
308
+ }
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Implementation of Node's `Stats`.
314
+ *
315
+ * Attribute descriptions are from `man 2 stat'
316
+ * @see http://nodejs.org/api/fs.html#fs_class_fs_stats
317
+ * @see http://man7.org/linux/man-pages/man2/stat.2.html
318
+ */
319
+ export class Stats extends StatsCommon<number> implements Node.Stats, StatsLike {
320
+ protected _isBigint = false;
321
+
322
+ /**
323
+ * Clones the stats object.
324
+ * @deprecated use `new Stats(stats)`
325
+ */
326
+ public static clone(stats: Stats): Stats {
327
+ return new Stats(stats);
328
+ }
329
+ }
330
+ Stats satisfies typeof Node.Stats;
331
+
332
+ /**
333
+ * Stats with bigint
334
+ * @todo Implement with bigint instead of wrapping Stats
335
+ */
336
+ export class BigIntStats extends StatsCommon<bigint> implements Node.BigIntStats, StatsLike {
337
+ protected _isBigint = true;
338
+
339
+ public atimeNs: bigint;
340
+ public mtimeNs: bigint;
341
+ public ctimeNs: bigint;
342
+ public birthtimeNs: bigint;
343
+
344
+ /**
345
+ * Clone a stats object.
346
+ * @deprecated use `new BigIntStats(stats)`
347
+ */
348
+ public static clone(stats: BigIntStats | Stats): BigIntStats {
349
+ return new BigIntStats(stats);
350
+ }
351
+ }
352
+
353
+ export class StatsFs implements Node.StatsFsBase<number> {
354
+ /** Type of file system. */
355
+ public type: number;
356
+ /** Optimal transfer block size. */
357
+ public bsize: number;
358
+ /** Total data blocks in file system. */
359
+ public blocks: number;
360
+ /** Free blocks in file system. */
361
+ public bfree: number;
362
+ /** Available blocks for unprivileged users */
363
+ public bavail: number;
364
+ /** Total file nodes in file system. */
365
+ public files: number;
366
+ /** Free file nodes in file system. */
367
+ public ffree: number;
368
+ }
369
+
370
+ export class BigIntStatsFs implements Node.StatsFsBase<bigint> {
371
+ /** Type of file system. */
372
+ public type: bigint;
373
+ /** Optimal transfer block size. */
374
+ public bsize: bigint;
375
+ /** Total data blocks in file system. */
376
+ public blocks: bigint;
377
+ /** Free blocks in file system. */
378
+ public bfree: bigint;
379
+ /** Available blocks for unprivileged users */
380
+ public bavail: bigint;
381
+ /** Total file nodes in file system. */
382
+ public files: bigint;
383
+ /** Free file nodes in file system. */
384
+ public ffree: bigint;
385
+ }