@zenfs/core 2.7.3 → 2.7.5
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/dist/internal/filesystem.d.ts +1 -0
- package/dist/internal/filesystem.js +18 -12
- package/dist/internal/inode.d.ts +12 -2
- package/dist/internal/inode.js +12 -2
- package/dist/internal/ioctl.d.ts +83 -25
- package/dist/internal/ioctl.js +50 -185
- package/dist/node/streams.d.ts +2 -2
- package/dist/node/streams.js +9 -3
- package/dist/vfs/file.js +16 -10
- package/dist/vfs/vnode.js +3 -15
- package/dist/vfs/xattr.d.ts +2 -0
- package/dist/vfs/xattr.js +5 -4
- package/package.json +6 -4
- package/tests/fs/streams.test.ts +60 -0
- package/tests/perf/append.ts +34 -0
- package/tests/perf/common.ts +31 -0
- package/tests/perf/config.json +84 -0
- package/tests/perf/fragmented-read.ts +41 -0
- package/tests/perf/fragmented-write.ts +37 -0
- package/tests/perf/stream-read.ts +48 -0
- package/tests/perf/stream-write.ts +31 -0
- package/tests/setup.ts +1 -1
- package/types/README.md +0 -1
- package/types/readable-stream.d.ts +0 -18
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
2
|
import { withErrno } from 'kerium';
|
|
3
3
|
import { ioctl_default_ops_async, ioctl_default_ops_sync } from './ioctl.js';
|
|
4
|
-
|
|
4
|
+
/** Default bytes per chunk for streamed reads, matching Node's default stream `highWaterMark` */
|
|
5
|
+
const defaultChunkSize = 0x10000;
|
|
5
6
|
/**
|
|
6
7
|
* Provides a consistent and easy to use internal API.
|
|
7
8
|
* Default implementations for `exists` and `existsSync` are included.
|
|
@@ -97,17 +98,22 @@ export class FileSystem {
|
|
|
97
98
|
* @privateRemarks The default implementation of `streamRead` uses "chunked" `read`s
|
|
98
99
|
*/
|
|
99
100
|
streamRead(path, options) {
|
|
101
|
+
const { chunkSize = defaultChunkSize } = options;
|
|
102
|
+
let offset = options.start ?? 0;
|
|
103
|
+
let end = options.end;
|
|
100
104
|
return new ReadableStream({
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const buffer = new Uint8Array(bytesRead);
|
|
107
|
-
await this.read(path, buffer, offset, offset + bytesRead).catch(controller.error.bind(controller));
|
|
108
|
-
controller.enqueue(buffer);
|
|
105
|
+
pull: async (controller) => {
|
|
106
|
+
end ??= (await this.stat(path)).size;
|
|
107
|
+
if (offset >= end) {
|
|
108
|
+
controller.close();
|
|
109
|
+
return;
|
|
109
110
|
}
|
|
110
|
-
|
|
111
|
+
const buffer = new Uint8Array(Math.min(chunkSize, end - offset));
|
|
112
|
+
await this.read(path, buffer, offset, offset + buffer.byteLength);
|
|
113
|
+
offset += buffer.byteLength;
|
|
114
|
+
controller.enqueue(buffer);
|
|
115
|
+
if (offset >= end)
|
|
116
|
+
controller.close();
|
|
111
117
|
},
|
|
112
118
|
type: 'bytes',
|
|
113
119
|
});
|
|
@@ -140,7 +146,7 @@ export class FileSystem {
|
|
|
140
146
|
const cmd = ioctl_default_ops_async[command];
|
|
141
147
|
if (!cmd)
|
|
142
148
|
throw withErrno('ENOTTY');
|
|
143
|
-
return await cmd(
|
|
149
|
+
return await cmd(context, ...args);
|
|
144
150
|
}
|
|
145
151
|
/** @internal */
|
|
146
152
|
ioctlSync(context, command, ...args) {
|
|
@@ -148,6 +154,6 @@ export class FileSystem {
|
|
|
148
154
|
const cmd = ioctl_default_ops_sync[command];
|
|
149
155
|
if (!cmd)
|
|
150
156
|
throw withErrno('ENOTTY');
|
|
151
|
-
return cmd(
|
|
157
|
+
return cmd(context, ...args);
|
|
152
158
|
}
|
|
153
159
|
}
|
package/dist/internal/inode.d.ts
CHANGED
|
@@ -145,9 +145,19 @@ export declare enum InodeFlags {
|
|
|
145
145
|
/** File is in use by the kernel (eg. fs/cachefiles) */
|
|
146
146
|
KernelFile = 131072
|
|
147
147
|
}
|
|
148
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* User visible flags
|
|
150
|
+
* These are actually masks for userspace `FileFlag`s, not `InodeFlags`.
|
|
151
|
+
* @deprecated Use the export from `/internal/ioctl`
|
|
152
|
+
* @todo [breaking] remove
|
|
153
|
+
*/
|
|
149
154
|
export declare const userVisibleFlags = 253951;
|
|
150
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* User modifiable flags
|
|
157
|
+
* These are actually masks for userspace `FileFlag`s, not `InodeFlags`.
|
|
158
|
+
* @deprecated Use the export from `/internal/ioctl`
|
|
159
|
+
* @todo [breaking] remove these re-exports
|
|
160
|
+
*/
|
|
151
161
|
export declare const userModifiableFlags = 229631;
|
|
152
162
|
declare const Inode_base: import("memium").StructConstructor<BufferView<any>>;
|
|
153
163
|
/**
|
package/dist/internal/inode.js
CHANGED
|
@@ -326,9 +326,19 @@ export var InodeFlags;
|
|
|
326
326
|
/** File is in use by the kernel (eg. fs/cachefiles) */
|
|
327
327
|
InodeFlags[InodeFlags["KernelFile"] = 131072] = "KernelFile";
|
|
328
328
|
})(InodeFlags || (InodeFlags = {}));
|
|
329
|
-
/**
|
|
329
|
+
/**
|
|
330
|
+
* User visible flags
|
|
331
|
+
* These are actually masks for userspace `FileFlag`s, not `InodeFlags`.
|
|
332
|
+
* @deprecated Use the export from `/internal/ioctl`
|
|
333
|
+
* @todo [breaking] remove
|
|
334
|
+
*/
|
|
330
335
|
export const userVisibleFlags = 0x0003dfff;
|
|
331
|
-
/**
|
|
336
|
+
/**
|
|
337
|
+
* User modifiable flags
|
|
338
|
+
* These are actually masks for userspace `FileFlag`s, not `InodeFlags`.
|
|
339
|
+
* @deprecated Use the export from `/internal/ioctl`
|
|
340
|
+
* @todo [breaking] remove these re-exports
|
|
341
|
+
*/
|
|
332
342
|
export const userModifiableFlags = 0x000380ff;
|
|
333
343
|
/**
|
|
334
344
|
* Generic inode definition that can easily be serialized.
|
package/dist/internal/ioctl.d.ts
CHANGED
|
@@ -1,38 +1,96 @@
|
|
|
1
|
-
import { BufferView } from 'utilium/buffer';
|
|
2
1
|
import type { FileSystem } from './filesystem.js';
|
|
3
2
|
import type { InodeLike } from './inode.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Inode flags (FS_IOC_GETFLAGS / FS_IOC_SETFLAGS)
|
|
5
|
+
* @see `FS_*_FL` in `include/uapi/linux/fs.h` (around L250)
|
|
6
|
+
* @experimental
|
|
7
|
+
*/
|
|
8
|
+
export declare enum FileFlag {
|
|
9
|
+
/** Secure deletion */
|
|
10
|
+
SecureRm = 1,
|
|
11
|
+
/** Undelete */
|
|
12
|
+
Undelete = 2,
|
|
13
|
+
/** Compress file */
|
|
14
|
+
Compress = 4,
|
|
15
|
+
/** Synchronous updates */
|
|
16
|
+
Sync = 8,
|
|
17
|
+
/** Immutable file */
|
|
18
|
+
Immutable = 16,
|
|
19
|
+
/** Writes to file may only append */
|
|
20
|
+
Append = 32,
|
|
21
|
+
/** do not dump file */
|
|
22
|
+
NoDump = 64,
|
|
23
|
+
/** do not update atime */
|
|
24
|
+
NoAtime = 128,
|
|
25
|
+
Dirty = 256,
|
|
26
|
+
/** One or more compressed clusters */
|
|
27
|
+
CompressBlk = 512,
|
|
28
|
+
/** Don't compress */
|
|
29
|
+
NoCompress = 1024,
|
|
30
|
+
/** Encrypted file */
|
|
31
|
+
Encrypt = 2048,
|
|
32
|
+
/** btree format dir */
|
|
33
|
+
Btree = 4096,
|
|
34
|
+
/** hash-indexed directory */
|
|
35
|
+
Index = 4096,
|
|
36
|
+
/** AFS directory */
|
|
37
|
+
IMagic = 8192,
|
|
38
|
+
/** Reserved for ext3 */
|
|
39
|
+
JournalData = 16384,
|
|
40
|
+
/** file tail should not be merged */
|
|
41
|
+
NoTail = 32768,
|
|
42
|
+
/** dirsync behaviour (directories only) */
|
|
43
|
+
DirSync = 65536,
|
|
44
|
+
/** Top of directory hierarchies*/
|
|
45
|
+
TopDir = 131072,
|
|
46
|
+
/** Reserved for ext4 */
|
|
47
|
+
HugeFile = 262144,
|
|
48
|
+
/** Extents */
|
|
49
|
+
Extent = 524288,
|
|
50
|
+
/** Verity protected inode */
|
|
51
|
+
Verity = 1048576,
|
|
52
|
+
/** Inode used for large EA */
|
|
53
|
+
EaInode = 2097152,
|
|
54
|
+
/** Reserved for ext4 */
|
|
55
|
+
EofBlocks = 4194304,
|
|
56
|
+
/** Do not cow file */
|
|
57
|
+
NoCow = 8388608,
|
|
58
|
+
/** Inode is DAX */
|
|
59
|
+
Dax = 33554432,
|
|
60
|
+
/** Reserved for ext4 */
|
|
61
|
+
InlineData = 268435456,
|
|
62
|
+
/** Create with parents projid */
|
|
63
|
+
ProjInherit = 536870912,
|
|
64
|
+
/** Folder is case insensitive */
|
|
65
|
+
CaseFold = 1073741824,
|
|
66
|
+
/** reserved for ext2 lib */
|
|
67
|
+
Reserved = 2147483648
|
|
19
68
|
}
|
|
69
|
+
/** User visible flags */
|
|
70
|
+
export declare const userVisibleFlags = 253951;
|
|
71
|
+
/** User modifiable flags */
|
|
72
|
+
export declare const userModifiableFlags = 229631;
|
|
20
73
|
/**
|
|
21
74
|
* `FS_IOC_*` commands for {@link ioctl | `ioctl`}
|
|
22
75
|
* @remarks
|
|
23
76
|
* These are computed from a script since constant values are needed for enum member types
|
|
77
|
+
* @todo [breaking] remove deprecated flags and handlers
|
|
24
78
|
*/
|
|
25
79
|
export declare enum IOC {
|
|
26
80
|
GetFlags = 2148034049,
|
|
27
81
|
SetFlags = 1074292226,
|
|
28
82
|
GetVersion = 2148038145,
|
|
29
83
|
SetVersion = 1074296322,
|
|
84
|
+
/** @deprecated */
|
|
30
85
|
Fiemap = 3223348747,
|
|
86
|
+
/** @deprecated */
|
|
31
87
|
GetXattr = 2149341215,
|
|
88
|
+
/** @deprecated */
|
|
32
89
|
SetXattr = 1075599392,
|
|
33
90
|
GetLabel = 2164298801,
|
|
34
91
|
SetLabel = 1090556978,
|
|
35
92
|
GetUUID = 2148603136,
|
|
93
|
+
/** @deprecated */
|
|
36
94
|
GetSysfsPath = 2155943169
|
|
37
95
|
}
|
|
38
96
|
/**
|
|
@@ -82,12 +140,12 @@ export declare const ioctl_default_ops: {
|
|
|
82
140
|
2148038145($: IoctlContext): number;
|
|
83
141
|
2147776001($: IoctlContext): number;
|
|
84
142
|
3223348747(): never;
|
|
85
|
-
2149341215(
|
|
86
|
-
1075599392(
|
|
143
|
+
2149341215(): never;
|
|
144
|
+
1075599392(): never;
|
|
87
145
|
2164298801($: IoctlContext): string | undefined;
|
|
88
146
|
1090556978($: IoctlContext, label: string): void;
|
|
89
147
|
2148603136($: IoctlContext): string;
|
|
90
|
-
2155943169(
|
|
148
|
+
2155943169(): never;
|
|
91
149
|
};
|
|
92
150
|
/**
|
|
93
151
|
* @internal
|
|
@@ -103,12 +161,12 @@ export declare const ioctl_default_ops_async: {
|
|
|
103
161
|
2148038145($: IoctlContext): number;
|
|
104
162
|
2147776001($: IoctlContext): number;
|
|
105
163
|
3223348747(): never;
|
|
106
|
-
2149341215(
|
|
107
|
-
1075599392(
|
|
164
|
+
2149341215(): never;
|
|
165
|
+
1075599392(): never;
|
|
108
166
|
2164298801($: IoctlContext): string | undefined;
|
|
109
167
|
1090556978($: IoctlContext, label: string): void;
|
|
110
168
|
2148603136($: IoctlContext): string;
|
|
111
|
-
2155943169(
|
|
169
|
+
2155943169(): never;
|
|
112
170
|
};
|
|
113
171
|
type _IoctlOpsAsync = typeof ioctl_default_ops_async;
|
|
114
172
|
/**
|
|
@@ -131,12 +189,12 @@ export declare const ioctl_default_ops_sync: {
|
|
|
131
189
|
2148038145($: IoctlContext): number;
|
|
132
190
|
2147776001($: IoctlContext): number;
|
|
133
191
|
3223348747(): never;
|
|
134
|
-
2149341215(
|
|
135
|
-
1075599392(
|
|
192
|
+
2149341215(): never;
|
|
193
|
+
1075599392(): never;
|
|
136
194
|
2164298801($: IoctlContext): string | undefined;
|
|
137
195
|
1090556978($: IoctlContext, label: string): void;
|
|
138
196
|
2148603136($: IoctlContext): string;
|
|
139
|
-
2155943169(
|
|
197
|
+
2155943169(): never;
|
|
140
198
|
};
|
|
141
199
|
type _IoctlOpsSync = typeof ioctl_default_ops_sync;
|
|
142
200
|
/**
|
package/dist/internal/ioctl.js
CHANGED
|
@@ -1,178 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
3
|
-
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
4
|
-
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
5
|
-
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
6
|
-
var _, done = false;
|
|
7
|
-
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
8
|
-
var context = {};
|
|
9
|
-
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
10
|
-
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
11
|
-
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
12
|
-
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
13
|
-
if (kind === "accessor") {
|
|
14
|
-
if (result === void 0) continue;
|
|
15
|
-
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
16
|
-
if (_ = accept(result.get)) descriptor.get = _;
|
|
17
|
-
if (_ = accept(result.set)) descriptor.set = _;
|
|
18
|
-
if (_ = accept(result.init)) initializers.unshift(_);
|
|
19
|
-
}
|
|
20
|
-
else if (_ = accept(result)) {
|
|
21
|
-
if (kind === "field") initializers.unshift(_);
|
|
22
|
-
else descriptor[key] = _;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
26
|
-
done = true;
|
|
27
|
-
};
|
|
28
|
-
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
29
|
-
var useValue = arguments.length > 2;
|
|
30
|
-
for (var i = 0; i < initializers.length; i++) {
|
|
31
|
-
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
32
|
-
}
|
|
33
|
-
return useValue ? value : void 0;
|
|
34
|
-
};
|
|
35
|
-
import { Errno, Exception, withErrno } from 'kerium';
|
|
36
|
-
import { sizeof } from 'memium';
|
|
37
|
-
import { $from, struct, types as t } from 'memium/decorators';
|
|
38
|
-
import { _throw } from 'utilium';
|
|
39
|
-
import { BufferView } from 'utilium/buffer';
|
|
1
|
+
import { withErrno } from 'kerium';
|
|
40
2
|
import { InodeFlags } from './inode.js';
|
|
41
|
-
/*
|
|
42
|
-
* Flags for the fsxattr.xflags field
|
|
43
|
-
*/
|
|
44
|
-
var XFlag;
|
|
45
|
-
(function (XFlag) {
|
|
46
|
-
/** data in realtime volume */
|
|
47
|
-
XFlag[XFlag["RealTime"] = 1] = "RealTime";
|
|
48
|
-
/** preallocated file extents */
|
|
49
|
-
XFlag[XFlag["PreAlloc"] = 2] = "PreAlloc";
|
|
50
|
-
/** file cannot be modified */
|
|
51
|
-
XFlag[XFlag["Immutable"] = 8] = "Immutable";
|
|
52
|
-
/** all writes append */
|
|
53
|
-
XFlag[XFlag["Append"] = 16] = "Append";
|
|
54
|
-
/** all writes synchronous */
|
|
55
|
-
XFlag[XFlag["Sync"] = 32] = "Sync";
|
|
56
|
-
/** do not update access time */
|
|
57
|
-
XFlag[XFlag["NoAtime"] = 64] = "NoAtime";
|
|
58
|
-
/** do not include in backups */
|
|
59
|
-
XFlag[XFlag["NoDump"] = 128] = "NoDump";
|
|
60
|
-
/** create with rt bit set */
|
|
61
|
-
XFlag[XFlag["RtInherit"] = 256] = "RtInherit";
|
|
62
|
-
/** create with parents projid */
|
|
63
|
-
XFlag[XFlag["ProjInherit"] = 512] = "ProjInherit";
|
|
64
|
-
/** disallow symlink creation */
|
|
65
|
-
XFlag[XFlag["NoSymlinks"] = 1024] = "NoSymlinks";
|
|
66
|
-
/** extent size allocator hint */
|
|
67
|
-
XFlag[XFlag["ExtSize"] = 2048] = "ExtSize";
|
|
68
|
-
/** inherit inode extent size */
|
|
69
|
-
XFlag[XFlag["ExtSzInherit"] = 4096] = "ExtSzInherit";
|
|
70
|
-
/** do not defragment */
|
|
71
|
-
XFlag[XFlag["NoDefrag"] = 8192] = "NoDefrag";
|
|
72
|
-
/** use filestream allocator */
|
|
73
|
-
XFlag[XFlag["FileStream"] = 16384] = "FileStream";
|
|
74
|
-
/** use DAX for IO */
|
|
75
|
-
XFlag[XFlag["Dax"] = 32768] = "Dax";
|
|
76
|
-
/** CoW extent size allocator hint */
|
|
77
|
-
XFlag[XFlag["CowExtSize"] = 65536] = "CowExtSize";
|
|
78
|
-
/** no DIFLAG for this */
|
|
79
|
-
XFlag[XFlag["HasAttr"] = 2147483648] = "HasAttr";
|
|
80
|
-
})(XFlag || (XFlag = {}));
|
|
81
|
-
let fsxattr = (() => {
|
|
82
|
-
var _a, _b, _c, _d, _e;
|
|
83
|
-
let _classDecorators = [struct()];
|
|
84
|
-
let _classDescriptor;
|
|
85
|
-
let _classExtraInitializers = [];
|
|
86
|
-
let _classThis;
|
|
87
|
-
let _classSuper = $from(BufferView);
|
|
88
|
-
let _xflags_decorators;
|
|
89
|
-
let _xflags_initializers = [];
|
|
90
|
-
let _xflags_extraInitializers = [];
|
|
91
|
-
let _extsize_decorators;
|
|
92
|
-
let _extsize_initializers = [];
|
|
93
|
-
let _extsize_extraInitializers = [];
|
|
94
|
-
let _nextents_decorators;
|
|
95
|
-
let _nextents_initializers = [];
|
|
96
|
-
let _nextents_extraInitializers = [];
|
|
97
|
-
let _projid_decorators;
|
|
98
|
-
let _projid_initializers = [];
|
|
99
|
-
let _projid_extraInitializers = [];
|
|
100
|
-
let _cowextsize_decorators;
|
|
101
|
-
let _cowextsize_initializers = [];
|
|
102
|
-
let _cowextsize_extraInitializers = [];
|
|
103
|
-
let _pad_decorators;
|
|
104
|
-
let _pad_initializers = [];
|
|
105
|
-
let _pad_extraInitializers = [];
|
|
106
|
-
var fsxattr = class extends _classSuper {
|
|
107
|
-
static { _classThis = this; }
|
|
108
|
-
static {
|
|
109
|
-
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
110
|
-
_xflags_decorators = [(_a = t).uint32.bind(_a)];
|
|
111
|
-
_extsize_decorators = [(_b = t).uint32.bind(_b)];
|
|
112
|
-
_nextents_decorators = [(_c = t).uint32.bind(_c)];
|
|
113
|
-
_projid_decorators = [(_d = t).uint32.bind(_d)];
|
|
114
|
-
_cowextsize_decorators = [(_e = t).uint32.bind(_e)];
|
|
115
|
-
_pad_decorators = [t.char(8)];
|
|
116
|
-
__esDecorate(this, null, _xflags_decorators, { kind: "accessor", name: "xflags", static: false, private: false, access: { has: obj => "xflags" in obj, get: obj => obj.xflags, set: (obj, value) => { obj.xflags = value; } }, metadata: _metadata }, _xflags_initializers, _xflags_extraInitializers);
|
|
117
|
-
__esDecorate(this, null, _extsize_decorators, { kind: "accessor", name: "extsize", static: false, private: false, access: { has: obj => "extsize" in obj, get: obj => obj.extsize, set: (obj, value) => { obj.extsize = value; } }, metadata: _metadata }, _extsize_initializers, _extsize_extraInitializers);
|
|
118
|
-
__esDecorate(this, null, _nextents_decorators, { kind: "accessor", name: "nextents", static: false, private: false, access: { has: obj => "nextents" in obj, get: obj => obj.nextents, set: (obj, value) => { obj.nextents = value; } }, metadata: _metadata }, _nextents_initializers, _nextents_extraInitializers);
|
|
119
|
-
__esDecorate(this, null, _projid_decorators, { kind: "accessor", name: "projid", static: false, private: false, access: { has: obj => "projid" in obj, get: obj => obj.projid, set: (obj, value) => { obj.projid = value; } }, metadata: _metadata }, _projid_initializers, _projid_extraInitializers);
|
|
120
|
-
__esDecorate(this, null, _cowextsize_decorators, { kind: "accessor", name: "cowextsize", static: false, private: false, access: { has: obj => "cowextsize" in obj, get: obj => obj.cowextsize, set: (obj, value) => { obj.cowextsize = value; } }, metadata: _metadata }, _cowextsize_initializers, _cowextsize_extraInitializers);
|
|
121
|
-
__esDecorate(this, null, _pad_decorators, { kind: "accessor", name: "pad", static: false, private: false, access: { has: obj => "pad" in obj, get: obj => obj.pad, set: (obj, value) => { obj.pad = value; } }, metadata: _metadata }, _pad_initializers, _pad_extraInitializers);
|
|
122
|
-
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
123
|
-
fsxattr = _classThis = _classDescriptor.value;
|
|
124
|
-
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
125
|
-
}
|
|
126
|
-
static name = 'fsxattr';
|
|
127
|
-
#xflags_accessor_storage = __runInitializers(this, _xflags_initializers, void 0);
|
|
128
|
-
/** xflags field value */
|
|
129
|
-
get xflags() { return this.#xflags_accessor_storage; }
|
|
130
|
-
set xflags(value) { this.#xflags_accessor_storage = value; }
|
|
131
|
-
#extsize_accessor_storage = (__runInitializers(this, _xflags_extraInitializers), __runInitializers(this, _extsize_initializers, void 0));
|
|
132
|
-
/** extsize field value */
|
|
133
|
-
get extsize() { return this.#extsize_accessor_storage; }
|
|
134
|
-
set extsize(value) { this.#extsize_accessor_storage = value; }
|
|
135
|
-
#nextents_accessor_storage = (__runInitializers(this, _extsize_extraInitializers), __runInitializers(this, _nextents_initializers, void 0));
|
|
136
|
-
/** nextents field value */
|
|
137
|
-
get nextents() { return this.#nextents_accessor_storage; }
|
|
138
|
-
set nextents(value) { this.#nextents_accessor_storage = value; }
|
|
139
|
-
#projid_accessor_storage = (__runInitializers(this, _nextents_extraInitializers), __runInitializers(this, _projid_initializers, void 0));
|
|
140
|
-
/** project identifier */
|
|
141
|
-
get projid() { return this.#projid_accessor_storage; }
|
|
142
|
-
set projid(value) { this.#projid_accessor_storage = value; }
|
|
143
|
-
#cowextsize_accessor_storage = (__runInitializers(this, _projid_extraInitializers), __runInitializers(this, _cowextsize_initializers, void 0));
|
|
144
|
-
/** CoW extsize field value */
|
|
145
|
-
get cowextsize() { return this.#cowextsize_accessor_storage; }
|
|
146
|
-
set cowextsize(value) { this.#cowextsize_accessor_storage = value; }
|
|
147
|
-
#pad_accessor_storage = (__runInitializers(this, _cowextsize_extraInitializers), __runInitializers(this, _pad_initializers, []));
|
|
148
|
-
get pad() { return this.#pad_accessor_storage; }
|
|
149
|
-
set pad(value) { this.#pad_accessor_storage = value; }
|
|
150
|
-
constructor(inode = _throw(new Exception(Errno.EINVAL, 'fsxattr must be initialized with an inode'))) {
|
|
151
|
-
super(new ArrayBuffer(sizeof(fsxattr)));
|
|
152
|
-
__runInitializers(this, _pad_extraInitializers);
|
|
153
|
-
this.extsize = inode.size;
|
|
154
|
-
this.nextents = 1;
|
|
155
|
-
this.projid = inode.uid;
|
|
156
|
-
this.cowextsize = inode.size;
|
|
157
|
-
for (const name of Object.keys(InodeFlags)) {
|
|
158
|
-
if (!((inode.flags || 0) & InodeFlags[name]))
|
|
159
|
-
continue;
|
|
160
|
-
if (name in XFlag)
|
|
161
|
-
this.xflags |= XFlag[name];
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
static {
|
|
165
|
-
__runInitializers(_classThis, _classExtraInitializers);
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
return fsxattr = _classThis;
|
|
169
|
-
})();
|
|
170
3
|
/**
|
|
171
4
|
* Inode flags (FS_IOC_GETFLAGS / FS_IOC_SETFLAGS)
|
|
172
5
|
* @see `FS_*_FL` in `include/uapi/linux/fs.h` (around L250)
|
|
173
6
|
* @experimental
|
|
174
7
|
*/
|
|
175
|
-
var FileFlag;
|
|
8
|
+
export var FileFlag;
|
|
176
9
|
(function (FileFlag) {
|
|
177
10
|
/** Secure deletion */
|
|
178
11
|
FileFlag[FileFlag["SecureRm"] = 1] = "SecureRm";
|
|
@@ -237,10 +70,43 @@ var FileFlag;
|
|
|
237
70
|
/** reserved for ext2 lib */
|
|
238
71
|
FileFlag[FileFlag["Reserved"] = 2147483648] = "Reserved";
|
|
239
72
|
})(FileFlag || (FileFlag = {}));
|
|
73
|
+
/** User visible flags */
|
|
74
|
+
export const userVisibleFlags = 0x0003dfff;
|
|
75
|
+
/** User modifiable flags */
|
|
76
|
+
export const userModifiableFlags = 0x000380ff;
|
|
77
|
+
const flagPairs = [
|
|
78
|
+
[FileFlag.Sync, InodeFlags.Sync],
|
|
79
|
+
[FileFlag.Immutable, InodeFlags.Immutable],
|
|
80
|
+
[FileFlag.Append, InodeFlags.Append],
|
|
81
|
+
[FileFlag.NoAtime, InodeFlags.NoAtime],
|
|
82
|
+
[FileFlag.Encrypt, InodeFlags.Encrypted],
|
|
83
|
+
[FileFlag.DirSync, InodeFlags.Dirsync],
|
|
84
|
+
[FileFlag.Verity, InodeFlags.Verity],
|
|
85
|
+
[FileFlag.Dax, InodeFlags.DAX],
|
|
86
|
+
[FileFlag.CaseFold, InodeFlags.CaseFold],
|
|
87
|
+
];
|
|
88
|
+
const supportedFlags = flagPairs.reduce((all, [flag]) => all | flag, 0), settableFlags = flagPairs.reduce((all, [, flag]) => all | flag, 0);
|
|
89
|
+
function toFileFlags(flags) {
|
|
90
|
+
let value = 0;
|
|
91
|
+
for (const [file, inode] of flagPairs)
|
|
92
|
+
if (flags & inode)
|
|
93
|
+
value |= file;
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
function setFlags($, flags) {
|
|
97
|
+
if (flags & ~supportedFlags)
|
|
98
|
+
throw withErrno('ENOTSUP', 'Unsupported file flags');
|
|
99
|
+
let value = 0;
|
|
100
|
+
for (const [file, inode] of flagPairs)
|
|
101
|
+
if (flags & file)
|
|
102
|
+
value |= inode;
|
|
103
|
+
$.inode.flags = (($.inode.flags || 0) & ~settableFlags) | value;
|
|
104
|
+
}
|
|
240
105
|
/**
|
|
241
106
|
* `FS_IOC_*` commands for {@link ioctl | `ioctl`}
|
|
242
107
|
* @remarks
|
|
243
108
|
* These are computed from a script since constant values are needed for enum member types
|
|
109
|
+
* @todo [breaking] remove deprecated flags and handlers
|
|
244
110
|
*/
|
|
245
111
|
export var IOC;
|
|
246
112
|
(function (IOC) {
|
|
@@ -248,12 +114,16 @@ export var IOC;
|
|
|
248
114
|
IOC[IOC["SetFlags"] = 1074292226] = "SetFlags";
|
|
249
115
|
IOC[IOC["GetVersion"] = 2148038145] = "GetVersion";
|
|
250
116
|
IOC[IOC["SetVersion"] = 1074296322] = "SetVersion";
|
|
117
|
+
/** @deprecated */
|
|
251
118
|
IOC[IOC["Fiemap"] = 3223348747] = "Fiemap";
|
|
119
|
+
/** @deprecated */
|
|
252
120
|
IOC[IOC["GetXattr"] = 2149341215] = "GetXattr";
|
|
121
|
+
/** @deprecated */
|
|
253
122
|
IOC[IOC["SetXattr"] = 1075599392] = "SetXattr";
|
|
254
123
|
IOC[IOC["GetLabel"] = 2164298801] = "GetLabel";
|
|
255
124
|
IOC[IOC["SetLabel"] = 1090556978] = "SetLabel";
|
|
256
125
|
IOC[IOC["GetUUID"] = 2148603136] = "GetUUID";
|
|
126
|
+
/** @deprecated */
|
|
257
127
|
IOC[IOC["GetSysfsPath"] = 2155943169] = "GetSysfsPath";
|
|
258
128
|
})(IOC || (IOC = {}));
|
|
259
129
|
/**
|
|
@@ -276,12 +146,12 @@ export const ioctl_default_ops = {
|
|
|
276
146
|
[IOC.GetFlags]($) {
|
|
277
147
|
if (typeof $.inode.flags !== 'number')
|
|
278
148
|
throw withErrno('ENOTTY');
|
|
279
|
-
return $.inode.flags;
|
|
149
|
+
return toFileFlags($.inode.flags);
|
|
280
150
|
},
|
|
281
151
|
[IOC32.GetFlags]($) {
|
|
282
152
|
if (typeof $.inode.flags !== 'number')
|
|
283
153
|
throw withErrno('ENOTTY');
|
|
284
|
-
return $.inode.flags;
|
|
154
|
+
return toFileFlags($.inode.flags);
|
|
285
155
|
},
|
|
286
156
|
[IOC.GetVersion]($) {
|
|
287
157
|
if (typeof $.inode.version !== 'number')
|
|
@@ -296,10 +166,10 @@ export const ioctl_default_ops = {
|
|
|
296
166
|
[IOC.Fiemap]() {
|
|
297
167
|
throw withErrno('ENOTSUP');
|
|
298
168
|
},
|
|
299
|
-
[IOC.GetXattr](
|
|
300
|
-
|
|
169
|
+
[IOC.GetXattr]() {
|
|
170
|
+
throw withErrno('ENOTSUP');
|
|
301
171
|
},
|
|
302
|
-
[IOC.SetXattr](
|
|
172
|
+
[IOC.SetXattr]() {
|
|
303
173
|
throw withErrno('ENOTSUP');
|
|
304
174
|
},
|
|
305
175
|
[IOC.GetLabel]($) {
|
|
@@ -311,13 +181,8 @@ export const ioctl_default_ops = {
|
|
|
311
181
|
[IOC.GetUUID]($) {
|
|
312
182
|
return $.fs.uuid;
|
|
313
183
|
},
|
|
314
|
-
[IOC.GetSysfsPath](
|
|
315
|
-
|
|
316
|
-
* Returns the path component under /sys/fs/ that refers to this filesystem;
|
|
317
|
-
* also /sys/kernel/debug/ for filesystems with debugfs exports
|
|
318
|
-
* @todo Implement sysfs and have each FS implement the /sys/fs/<name> tree
|
|
319
|
-
*/
|
|
320
|
-
return `/sys/fs/${$.fs.name}/${$.fs.uuid}`;
|
|
184
|
+
[IOC.GetSysfsPath]() {
|
|
185
|
+
throw withErrno('ENOTSUP');
|
|
321
186
|
},
|
|
322
187
|
};
|
|
323
188
|
/**
|
|
@@ -327,11 +192,11 @@ export const ioctl_default_ops = {
|
|
|
327
192
|
export const ioctl_default_ops_async = {
|
|
328
193
|
...ioctl_default_ops,
|
|
329
194
|
async [IOC.SetFlags]($, flags) {
|
|
330
|
-
|
|
195
|
+
setFlags($, flags);
|
|
331
196
|
await $.fs.touch($.path, $.inode);
|
|
332
197
|
},
|
|
333
198
|
async [IOC32.SetFlags]($, flags) {
|
|
334
|
-
|
|
199
|
+
setFlags($, flags);
|
|
335
200
|
await $.fs.touch($.path, $.inode);
|
|
336
201
|
},
|
|
337
202
|
async [IOC.SetVersion]($, version) {
|
|
@@ -350,11 +215,11 @@ export const ioctl_default_ops_async = {
|
|
|
350
215
|
export const ioctl_default_ops_sync = {
|
|
351
216
|
...ioctl_default_ops,
|
|
352
217
|
[IOC.SetFlags]($, flags) {
|
|
353
|
-
|
|
218
|
+
setFlags($, flags);
|
|
354
219
|
$.fs.touchSync($.path, $.inode);
|
|
355
220
|
},
|
|
356
221
|
[IOC32.SetFlags]($, flags) {
|
|
357
|
-
|
|
222
|
+
setFlags($, flags);
|
|
358
223
|
$.fs.touchSync($.path, $.inode);
|
|
359
224
|
},
|
|
360
225
|
[IOC.SetVersion]($, version) {
|
package/dist/node/streams.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/// <reference path="../../types/readable-stream.d.ts" preserve="true" />
|
|
2
1
|
import type { Abortable } from 'node:events';
|
|
3
2
|
import type * as fs from 'node:fs';
|
|
4
3
|
import type { CreateReadStreamOptions, CreateWriteStreamOptions } from 'node:fs/promises';
|
|
5
4
|
import type { Callback } from '../utils.js';
|
|
6
5
|
import type { FileHandle } from './promises.js';
|
|
7
|
-
import { Readable
|
|
6
|
+
import { Readable } from '@zenfs/streams/readable';
|
|
7
|
+
import { Writable } from '@zenfs/streams/writable';
|
|
8
8
|
interface FSImplementation {
|
|
9
9
|
open?: (...args: unknown[]) => unknown;
|
|
10
10
|
close?: (...args: unknown[]) => unknown;
|
package/dist/node/streams.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { eos } from '@zenfs/streams/end-of-stream';
|
|
2
|
+
import { Readable } from '@zenfs/streams/readable';
|
|
3
|
+
import { Writable } from '@zenfs/streams/writable';
|
|
1
4
|
import { Errno, Exception, UV } from 'kerium';
|
|
2
5
|
import { warn } from 'kerium/log';
|
|
3
|
-
import { finished, Readable, Writable } from 'readable-stream';
|
|
4
6
|
/**
|
|
5
7
|
* A ReadStream implementation that wraps an underlying global ReadableStream.
|
|
6
8
|
*/
|
|
@@ -16,7 +18,11 @@ export class ReadStream extends Readable {
|
|
|
16
18
|
.then(handle => {
|
|
17
19
|
this._path = handle['vfs'].path;
|
|
18
20
|
// `end` is inclusive for streams, but the internal range is not
|
|
19
|
-
const internal = handle.readableWebStream({
|
|
21
|
+
const internal = handle.readableWebStream({
|
|
22
|
+
start: opts.start,
|
|
23
|
+
end: opts.end === undefined ? undefined : opts.end + 1,
|
|
24
|
+
chunkSize: opts.highWaterMark,
|
|
25
|
+
});
|
|
20
26
|
this.reader = internal.getReader();
|
|
21
27
|
this.pending = false;
|
|
22
28
|
})
|
|
@@ -46,7 +52,7 @@ export class ReadStream extends Readable {
|
|
|
46
52
|
* Closing before the stream has finished reports `ERR_STREAM_PREMATURE_CLOSE`.
|
|
47
53
|
*/
|
|
48
54
|
close(callback = () => null) {
|
|
49
|
-
|
|
55
|
+
eos(this, error => callback(error ?? null));
|
|
50
56
|
this.destroy();
|
|
51
57
|
}
|
|
52
58
|
get path() {
|
package/dist/vfs/file.js
CHANGED
|
@@ -93,8 +93,8 @@ import { _chown, InodeFlags, isBlockDevice, isCharacterDevice } from '../interna
|
|
|
93
93
|
import '../polyfills.js';
|
|
94
94
|
import { validateFD } from '../utils.js';
|
|
95
95
|
import { cacheOf } from './vcache.js';
|
|
96
|
-
/**
|
|
97
|
-
const streamChunkSize =
|
|
96
|
+
/** Default bytes per chunk for streamed reads, matching Node's default stream `highWaterMark` */
|
|
97
|
+
const streamChunkSize = 0x10000;
|
|
98
98
|
/**
|
|
99
99
|
* @internal
|
|
100
100
|
*/
|
|
@@ -608,19 +608,25 @@ let Handle = (() => {
|
|
|
608
608
|
if (this.closed)
|
|
609
609
|
throw UV('EBADF', 'streamRead', this.path);
|
|
610
610
|
const { vnode } = this;
|
|
611
|
+
const { chunkSize = streamChunkSize } = options;
|
|
612
|
+
let offset = options.start ?? 0;
|
|
613
|
+
let end = options.end;
|
|
611
614
|
return new ReadableStream({
|
|
612
|
-
async
|
|
615
|
+
async pull(controller) {
|
|
613
616
|
const env_15 = { stack: [], error: void 0, hasError: false };
|
|
614
617
|
try {
|
|
615
618
|
const _ = __addDisposableResource(env_15, await vnode.lock('ro'), false);
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
await vnode.read(buffer, offset, offset + bytesRead).catch(controller.error.bind(controller));
|
|
621
|
-
controller.enqueue(buffer);
|
|
619
|
+
end ??= vnode.inode.size;
|
|
620
|
+
if (offset >= end) {
|
|
621
|
+
controller.close();
|
|
622
|
+
return;
|
|
622
623
|
}
|
|
623
|
-
|
|
624
|
+
const buffer = new Uint8Array(Math.min(chunkSize, end - offset));
|
|
625
|
+
await vnode.read(buffer, offset, offset + buffer.byteLength);
|
|
626
|
+
offset += buffer.byteLength;
|
|
627
|
+
controller.enqueue(buffer);
|
|
628
|
+
if (offset >= end)
|
|
629
|
+
controller.close();
|
|
624
630
|
}
|
|
625
631
|
catch (e_15) {
|
|
626
632
|
env_15.error = e_15;
|
package/dist/vfs/vnode.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
2
|
import { RwLockable } from 'kerium/locks';
|
|
3
|
-
import { Resource } from 'utilium/cache';
|
|
3
|
+
import { addRange, Resource } from 'utilium/cache';
|
|
4
4
|
import { InodeFlags, isBlockDevice, isCharacterDevice } from '../internal/inode.js';
|
|
5
5
|
/**
|
|
6
6
|
* A VFS-level node, roughly equivalent to Linux's in-memory `struct inode`.
|
|
@@ -57,17 +57,7 @@ export class VNode extends RwLockable {
|
|
|
57
57
|
}
|
|
58
58
|
/** Add a range to `dirtyRanges`, merging overlapping and adjacent ranges */
|
|
59
59
|
markDirty(start, end) {
|
|
60
|
-
this.dirtyRanges
|
|
61
|
-
this.dirtyRanges.sort((a, b) => a.start - b.start);
|
|
62
|
-
const merged = [];
|
|
63
|
-
for (const range of this.dirtyRanges) {
|
|
64
|
-
const last = merged.at(-1);
|
|
65
|
-
if (last && range.start <= last.end)
|
|
66
|
-
last.end = Math.max(last.end, range.end);
|
|
67
|
-
else
|
|
68
|
-
merged.push(range);
|
|
69
|
-
}
|
|
70
|
-
this.dirtyRanges = merged;
|
|
60
|
+
addRange(this.dirtyRanges, start, end);
|
|
71
61
|
}
|
|
72
62
|
/**
|
|
73
63
|
* Copy `[start, end)` from the cache into `buffer`.
|
|
@@ -75,12 +65,10 @@ export class VNode extends RwLockable {
|
|
|
75
65
|
*/
|
|
76
66
|
copyFromCache(buffer, start, end) {
|
|
77
67
|
let pos = start;
|
|
78
|
-
for (const region of this.data.
|
|
68
|
+
for (const region of this.data.regionsIn(start, end)) {
|
|
79
69
|
const regionEnd = region.offset + region.data.byteLength;
|
|
80
70
|
if (regionEnd <= pos)
|
|
81
71
|
continue;
|
|
82
|
-
if (region.offset >= end)
|
|
83
|
-
break;
|
|
84
72
|
if (region.offset > pos) {
|
|
85
73
|
buffer.fill(0, pos - start, region.offset - start);
|
|
86
74
|
pos = region.offset;
|
package/dist/vfs/xattr.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export interface SetOptions extends Options {
|
|
|
44
44
|
*/
|
|
45
45
|
replace?: boolean;
|
|
46
46
|
}
|
|
47
|
+
/** Attributes outside the `user` namespace that are supported. */
|
|
48
|
+
export declare const allowedRestrictedNames: Set<`user.${string}` | `trusted.${string}` | `system.${string}` | `security.${string}`>;
|
|
47
49
|
/**
|
|
48
50
|
* Gets the value of an extended attribute.
|
|
49
51
|
*
|
package/dist/vfs/xattr.js
CHANGED
|
@@ -6,14 +6,15 @@ import { normalizePath } from '../utils.js';
|
|
|
6
6
|
import { checkAccess } from './config.js';
|
|
7
7
|
import { R_OK, W_OK } from '../constants.js';
|
|
8
8
|
import { resolveMount } from './shared.js';
|
|
9
|
-
|
|
9
|
+
/** Attributes outside the `user` namespace that are supported. */
|
|
10
|
+
export const allowedRestrictedNames = new Set();
|
|
10
11
|
/**
|
|
11
12
|
* Check permission for the attribute name.
|
|
12
|
-
*
|
|
13
|
-
* @throws ENOTSUP for
|
|
13
|
+
* Only the 'user' namespace and whatever is in {@link allowedRestrictedNames} are supported.
|
|
14
|
+
* @throws ENOTSUP for any other attribute
|
|
14
15
|
*/
|
|
15
16
|
function checkName($, name, path, syscall) {
|
|
16
|
-
if (!name.startsWith('user.') && !
|
|
17
|
+
if (!name.startsWith('user.') && !allowedRestrictedNames.has(name))
|
|
17
18
|
throw UV('ENOTSUP', syscall, path);
|
|
18
19
|
}
|
|
19
20
|
export async function get(path, name, opt = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenfs/core",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.5",
|
|
4
4
|
"description": "A filesystem, anywhere",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -69,18 +69,19 @@
|
|
|
69
69
|
"lint": "eslint src tests",
|
|
70
70
|
"test": "npx -s zenfs-test --clean; npx -s zenfs-test -abcpn; tests/fetch/run.sh; npx -s zenfs-test --report-only",
|
|
71
71
|
"test:native": "npx -s zenfs-test tests/native-setup.ts --verbose",
|
|
72
|
+
"test:perf": "zbench",
|
|
72
73
|
"build": "tsc --build",
|
|
73
74
|
"dev": "tsc --build --watch",
|
|
74
75
|
"prepublishOnly": "npm run build"
|
|
75
76
|
},
|
|
76
77
|
"dependencies": {
|
|
77
78
|
"@types/node": "^26.0.0",
|
|
79
|
+
"@zenfs/streams": "^1.0.0",
|
|
78
80
|
"buffer": "^6.0.3",
|
|
79
81
|
"eventemitter3": "^5.0.1",
|
|
80
82
|
"kerium": "^1.5.0",
|
|
81
83
|
"memium": "^1.0.0",
|
|
82
|
-
"
|
|
83
|
-
"utilium": "^3.0.0"
|
|
84
|
+
"utilium": "^3.7.0"
|
|
84
85
|
},
|
|
85
86
|
"devDependencies": {
|
|
86
87
|
"@eslint/js": "^10.0.1",
|
|
@@ -91,7 +92,8 @@
|
|
|
91
92
|
"prettier": "^3.2.5",
|
|
92
93
|
"typedoc": "^0.28.18",
|
|
93
94
|
"typescript": "^6.0.0",
|
|
94
|
-
"typescript-eslint": "^8.58.0"
|
|
95
|
+
"typescript-eslint": "^8.58.0",
|
|
96
|
+
"zbench-js": "^0.2.0"
|
|
95
97
|
},
|
|
96
98
|
"prettier": {
|
|
97
99
|
"singleQuote": true,
|
package/tests/fs/streams.test.ts
CHANGED
|
@@ -141,4 +141,64 @@ suite('Streams', config('streams'), () => {
|
|
|
141
141
|
// Closing invalidates the descriptor, so it fails range validation
|
|
142
142
|
assert.throws(() => fileHandle.createWriteStream(), { code: 'ERR_OUT_OF_RANGE' });
|
|
143
143
|
});
|
|
144
|
+
|
|
145
|
+
// A file large enough to span several chunks, so the read path has to stitch
|
|
146
|
+
// them back together and the vnode cache has to grow to hold them.
|
|
147
|
+
const large = Buffer.alloc(200 * 1024);
|
|
148
|
+
for (let i = 0; i < large.length; i++) large[i] = (i * 31 + 7) & 0xff;
|
|
149
|
+
|
|
150
|
+
test('createReadStream reads a multi-chunk file byte for byte', async () => {
|
|
151
|
+
await fs.promises.writeFile('large.bin', large);
|
|
152
|
+
|
|
153
|
+
const chunks: Uint8Array[] = [];
|
|
154
|
+
for await (const chunk of fs.createReadStream('large.bin')) chunks.push(chunk as Uint8Array);
|
|
155
|
+
|
|
156
|
+
assert.deepEqual(Buffer.concat(chunks), large);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('createReadStream honors highWaterMark as the chunk size', async () => {
|
|
160
|
+
await fs.promises.writeFile('large.bin', large);
|
|
161
|
+
|
|
162
|
+
const highWaterMark = 64 * 1024;
|
|
163
|
+
const sizes: number[] = [];
|
|
164
|
+
for await (const chunk of fs.createReadStream('large.bin', { highWaterMark })) sizes.push((chunk as Uint8Array).byteLength);
|
|
165
|
+
|
|
166
|
+
// Every chunk but the last is a full highWaterMark, and they sum to the file
|
|
167
|
+
assert.deepEqual(sizes.slice(0, -1), Array(sizes.length - 1).fill(highWaterMark));
|
|
168
|
+
assert.equal(
|
|
169
|
+
sizes.reduce((a, b) => a + b, 0),
|
|
170
|
+
large.length
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('createReadStream with start and end spanning chunks', async () => {
|
|
175
|
+
await fs.promises.writeFile('large.bin', large);
|
|
176
|
+
|
|
177
|
+
// `end` is inclusive, and this range deliberately starts and stops mid-chunk
|
|
178
|
+
const start = 1000;
|
|
179
|
+
const end = 150_000;
|
|
180
|
+
const chunks: Uint8Array[] = [];
|
|
181
|
+
for await (const chunk of fs.createReadStream('large.bin', { start, end, highWaterMark: 8192 })) chunks.push(chunk as Uint8Array);
|
|
182
|
+
|
|
183
|
+
assert.deepEqual(Buffer.concat(chunks), large.subarray(start, end + 1));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('a read stream does not read past the end of the file', async () => {
|
|
187
|
+
await fs.promises.writeFile('small.bin', large.subarray(0, 10));
|
|
188
|
+
|
|
189
|
+
const chunks: Uint8Array[] = [];
|
|
190
|
+
for await (const chunk of fs.createReadStream('small.bin', { highWaterMark: 64 * 1024 })) chunks.push(chunk as Uint8Array);
|
|
191
|
+
|
|
192
|
+
assert.equal(chunks.length, 1);
|
|
193
|
+
assert.deepEqual(chunks[0], large.subarray(0, 10));
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('a read stream over an empty file ends without a chunk', async () => {
|
|
197
|
+
await fs.promises.writeFile('empty.bin', '');
|
|
198
|
+
|
|
199
|
+
const chunks: unknown[] = [];
|
|
200
|
+
for await (const chunk of fs.createReadStream('empty.bin')) chunks.push(chunk);
|
|
201
|
+
|
|
202
|
+
assert.deepEqual(chunks, []);
|
|
203
|
+
});
|
|
144
204
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
// Sequential appends into one region. Every write extends the same region, so a region that
|
|
3
|
+
// allocates exactly what it needs copies the whole file on each append.
|
|
4
|
+
import type { FileHandle } from '@zenfs/core/promises';
|
|
5
|
+
import { fs, mount, piece } from './common.ts';
|
|
6
|
+
|
|
7
|
+
interface Config {
|
|
8
|
+
writes: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface State {
|
|
12
|
+
data: Buffer;
|
|
13
|
+
handle?: FileHandle;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function setup(): Promise<State> {
|
|
17
|
+
await mount('perf-append');
|
|
18
|
+
return { data: Buffer.alloc(piece, 0x11) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function before(config: Config, state: State): Promise<void> {
|
|
22
|
+
state.handle = await fs.promises.open('/a', 'w');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function test(config: Config, state: State) {
|
|
26
|
+
for (let i = 0; i < config.writes; i++) await state.handle!.write(state.data, 0, state.data.length, i * piece);
|
|
27
|
+
|
|
28
|
+
return { writes: config.writes, bytes: config.writes * piece };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function after(config: Config, state: State): Promise<void> {
|
|
32
|
+
await state.handle!.close();
|
|
33
|
+
await fs.promises.rm('/a');
|
|
34
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
import { configureSingle, fs, InMemory } from '@zenfs/core';
|
|
3
|
+
|
|
4
|
+
export { fs };
|
|
5
|
+
|
|
6
|
+
export const KB = 1024;
|
|
7
|
+
export const MB = 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A piece and the stride between pieces, chosen so consecutive writes never merge into one region:
|
|
11
|
+
* the 12 KB hole between them is wider than `regionGapThreshold`.
|
|
12
|
+
*/
|
|
13
|
+
export const piece = 4 * KB;
|
|
14
|
+
export const stride = 16 * KB;
|
|
15
|
+
|
|
16
|
+
/** A fresh in-memory filesystem, so no configuration inherits another's cached vnodes. */
|
|
17
|
+
export async function mount(name: string): Promise<void> {
|
|
18
|
+
await configureSingle({ backend: InMemory, name });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Split a buffer the way a stream writer feeds it. */
|
|
22
|
+
export function chunks(buffer: Buffer, size: number): Buffer[] {
|
|
23
|
+
const out: Buffer[] = [];
|
|
24
|
+
for (let i = 0; i < buffer.length; i += size) out.push(buffer.subarray(i, Math.min(i + size, buffer.length)));
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Give the event loop a moment, so work queued by the previous phase never lands in the timed one. */
|
|
29
|
+
export function settle(): Promise<void> {
|
|
30
|
+
return new Promise(resolve => setTimeout(resolve, 50));
|
|
31
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"flags": {
|
|
3
|
+
"prep": ["writeFile", "stream"]
|
|
4
|
+
},
|
|
5
|
+
"tests": [
|
|
6
|
+
{
|
|
7
|
+
"path": "stream-read.ts",
|
|
8
|
+
"name": "Read with createReadStream",
|
|
9
|
+
"flags": ["prep"],
|
|
10
|
+
"measure": {
|
|
11
|
+
"bytes": { "throughput": "s", "aggregate_throughput": "s", "unit": "MB" },
|
|
12
|
+
"files": { "cost": true, "unit": "file" }
|
|
13
|
+
},
|
|
14
|
+
"configurations": [
|
|
15
|
+
{ "name": "4K x2000", "value": { "size": 4096, "count": 2000, "hwm": 65536 } },
|
|
16
|
+
{ "name": "256K x200", "value": { "size": 262144, "count": 200, "hwm": 65536 } },
|
|
17
|
+
{ "name": "8M x8", "cpu": 1, "mem": 1, "value": { "size": 8388608, "count": 8, "hwm": 65536 } },
|
|
18
|
+
{
|
|
19
|
+
"name": "8M x8, hwm 1M",
|
|
20
|
+
"cpu": 1,
|
|
21
|
+
"mem": 1,
|
|
22
|
+
"value": { "size": 8388608, "count": 8, "hwm": 1048576 }
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"path": "stream-write.ts",
|
|
28
|
+
"name": "Write with createWriteStream",
|
|
29
|
+
"measure": {
|
|
30
|
+
"bytes": { "throughput": "s", "aggregate_throughput": "s", "unit": "MB" },
|
|
31
|
+
"files": { "cost": true, "unit": "file" }
|
|
32
|
+
},
|
|
33
|
+
"configurations": [
|
|
34
|
+
{ "name": "4K x2000", "value": { "size": 4096, "count": 2000, "hwm": 65536 } },
|
|
35
|
+
{ "name": "256K x200", "value": { "size": 262144, "count": 200, "hwm": 65536 } },
|
|
36
|
+
{ "name": "8M x8", "cpu": 1, "mem": 1, "value": { "size": 8388608, "count": 8, "hwm": 65536 } },
|
|
37
|
+
{
|
|
38
|
+
"name": "8M x8, hwm 1M",
|
|
39
|
+
"cpu": 1,
|
|
40
|
+
"mem": 1,
|
|
41
|
+
"value": { "size": 8388608, "count": 8, "hwm": 1048576 }
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"path": "fragmented-read.ts",
|
|
47
|
+
"name": "Read across many cached regions",
|
|
48
|
+
"measure": {
|
|
49
|
+
"reads": { "cost": true, "aggregate_cost": true, "unit": "read" }
|
|
50
|
+
},
|
|
51
|
+
"configurations": [
|
|
52
|
+
{ "name": "128 regions", "value": { "regions": 128, "reads": 4000 } },
|
|
53
|
+
{ "name": "512 regions", "value": { "regions": 512, "reads": 4000 } },
|
|
54
|
+
{ "name": "2048 regions", "cpu": 1, "value": { "regions": 2048, "reads": 4000 } },
|
|
55
|
+
{ "name": "8192 regions", "cpu": 2, "mem": 1, "value": { "regions": 8192, "reads": 4000 } }
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"path": "fragmented-write.ts",
|
|
60
|
+
"name": "Scattered writes into one file",
|
|
61
|
+
"measure": {
|
|
62
|
+
"writes": { "cost": true, "aggregate_cost": true, "unit": "write" }
|
|
63
|
+
},
|
|
64
|
+
"configurations": [
|
|
65
|
+
{ "name": "512 writes", "value": { "writes": 512 } },
|
|
66
|
+
{ "name": "2048 writes", "cpu": 1, "value": { "writes": 2048 } },
|
|
67
|
+
{ "name": "8192 writes", "cpu": 2, "mem": 1, "value": { "writes": 8192 } }
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"path": "append.ts",
|
|
72
|
+
"name": "Append 4 KB at a time",
|
|
73
|
+
"measure": {
|
|
74
|
+
"bytes": { "throughput": "s", "unit": "MB" },
|
|
75
|
+
"writes": { "cost": true, "aggregate_cost": true, "unit": "write" }
|
|
76
|
+
},
|
|
77
|
+
"configurations": [
|
|
78
|
+
{ "name": "500 appends", "value": { "writes": 500 } },
|
|
79
|
+
{ "name": "2000 appends", "value": { "writes": 2000 } },
|
|
80
|
+
{ "name": "8000 appends", "cpu": 1, "value": { "writes": 8000 } }
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
// Reads spread across a file with many disjoint cached regions.
|
|
3
|
+
|
|
4
|
+
import type { FileHandle } from '@zenfs/core/promises';
|
|
5
|
+
import { fs, mount, piece, stride } from './common.ts';
|
|
6
|
+
|
|
7
|
+
interface Config {
|
|
8
|
+
/** Cached regions to build, one per scattered write */
|
|
9
|
+
regions: number;
|
|
10
|
+
reads: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface State {
|
|
14
|
+
handle: FileHandle;
|
|
15
|
+
target: Buffer;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function setup(config: Config): Promise<State> {
|
|
19
|
+
await mount('perf-fragmented-read');
|
|
20
|
+
|
|
21
|
+
await fs.promises.writeFile('/f', Buffer.alloc(config.regions * stride));
|
|
22
|
+
const handle = await fs.promises.open('/f', 'r+');
|
|
23
|
+
|
|
24
|
+
const data = Buffer.alloc(piece, 0xcd);
|
|
25
|
+
for (let i = 0; i < config.regions; i++) await handle.write(data, 0, data.length, i * stride);
|
|
26
|
+
|
|
27
|
+
return { handle, target: Buffer.allocUnsafe(piece) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function test(config: Config, state: State) {
|
|
31
|
+
// Backwards, so a scan that starts at region zero pays its worst case on every read
|
|
32
|
+
for (let i = 0; i < config.reads; i++)
|
|
33
|
+
await state.handle.read(state.target, 0, state.target.length, (config.regions - 1 - (i % config.regions)) * stride);
|
|
34
|
+
|
|
35
|
+
return { reads: config.reads, bytes: config.reads * piece };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function teardown(config: Config, state: State): Promise<void> {
|
|
39
|
+
await state.handle.close();
|
|
40
|
+
await fs.promises.rm('/f');
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
// Writes that each land in their own region, so the region list grows by one per write.
|
|
3
|
+
// Anything that walks the whole list — or re-sorts the dirty ranges — per write shows up as
|
|
4
|
+
// quadratic here.
|
|
5
|
+
import type { FileHandle } from '@zenfs/core/promises';
|
|
6
|
+
import { fs, mount, piece, stride } from './common.ts';
|
|
7
|
+
|
|
8
|
+
interface Config {
|
|
9
|
+
writes: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface State {
|
|
13
|
+
data: Buffer;
|
|
14
|
+
handle?: FileHandle;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function setup(): Promise<State> {
|
|
18
|
+
await mount('perf-fragmented-write');
|
|
19
|
+
return { data: Buffer.alloc(piece, 0xcd) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A fresh file each iteration: the point is the cost of building the region list, not of reusing it. */
|
|
23
|
+
export async function before(config: Config, state: State): Promise<void> {
|
|
24
|
+
await fs.promises.writeFile('/f', Buffer.alloc(config.writes * stride));
|
|
25
|
+
state.handle = await fs.promises.open('/f', 'r+');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function test(config: Config, state: State) {
|
|
29
|
+
for (let i = 0; i < config.writes; i++) await state.handle!.write(state.data, 0, state.data.length, i * stride);
|
|
30
|
+
|
|
31
|
+
return { writes: config.writes, bytes: config.writes * piece };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function after(config: Config, state: State): Promise<void> {
|
|
35
|
+
await state.handle!.close();
|
|
36
|
+
await fs.promises.rm('/f');
|
|
37
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
// Reading through `createReadStream`: the VFS cache lookup, `copyFromCache`, and the stream's own
|
|
3
|
+
// chunking, with the files written outside the timed window.
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
import { chunks, fs, mount, settle } from './common.ts';
|
|
6
|
+
|
|
7
|
+
interface Config {
|
|
8
|
+
size: number;
|
|
9
|
+
count: number;
|
|
10
|
+
/** Stream highWaterMark, which is also the read chunk size */
|
|
11
|
+
hwm: number;
|
|
12
|
+
prep: 'writeFile' | 'stream';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface State {
|
|
16
|
+
buffer: Buffer;
|
|
17
|
+
parts: Buffer[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function setup(config: Config): Promise<State> {
|
|
21
|
+
await mount('perf-stream-read');
|
|
22
|
+
const buffer = Buffer.alloc(config.size, 0xab);
|
|
23
|
+
return { buffer, parts: chunks(buffer, config.hwm) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function before(config: Config, state: State): Promise<void> {
|
|
27
|
+
for (let i = 0; i < config.count; i++) {
|
|
28
|
+
if (config.prep == 'stream') await pipeline(state.parts, fs.createWriteStream(`/w${i}`, { highWaterMark: config.hwm }));
|
|
29
|
+
else await fs.promises.writeFile(`/w${i}`, state.buffer);
|
|
30
|
+
}
|
|
31
|
+
await settle();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function test(config: Config) {
|
|
35
|
+
let bytes = 0;
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < config.count; i++) {
|
|
38
|
+
for await (const chunk of fs.createReadStream(`/w${i}`, { highWaterMark: config.hwm })) bytes += (chunk as Buffer).length;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (bytes != config.size * config.count) throw new Error(`short read: ${bytes} of ${config.size * config.count}`);
|
|
42
|
+
|
|
43
|
+
return { bytes, files: config.count };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function after(config: Config): Promise<void> {
|
|
47
|
+
for (let i = 0; i < config.count; i++) await fs.promises.rm(`/w${i}`);
|
|
48
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
// Writing through `createWriteStream`: how the cache grows as a file is appended to one chunk at a
|
|
3
|
+
// time. A region that reallocates exactly on every append makes this quadratic in the file's size.
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
import { chunks, fs, mount, settle } from './common.ts';
|
|
6
|
+
|
|
7
|
+
interface Config {
|
|
8
|
+
size: number;
|
|
9
|
+
count: number;
|
|
10
|
+
hwm: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface State {
|
|
14
|
+
parts: Buffer[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function setup(config: Config): Promise<State> {
|
|
18
|
+
await mount('perf-stream-write');
|
|
19
|
+
return { parts: chunks(Buffer.alloc(config.size, 0xab), config.hwm) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function test(config: Config, state: State) {
|
|
23
|
+
for (let i = 0; i < config.count; i++) await pipeline(state.parts, fs.createWriteStream(`/w${i}`, { highWaterMark: config.hwm }));
|
|
24
|
+
|
|
25
|
+
return { bytes: config.size * config.count, files: config.count };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function after(config: Config): Promise<void> {
|
|
29
|
+
for (let i = 0; i < config.count; i++) await fs.promises.rm(`/w${i}`);
|
|
30
|
+
await settle();
|
|
31
|
+
}
|
package/tests/setup.ts
CHANGED
|
@@ -8,7 +8,7 @@ export const data = join(import.meta.dirname, 'data');
|
|
|
8
8
|
|
|
9
9
|
export const tmp = join(import.meta.dirname, 'tmp');
|
|
10
10
|
|
|
11
|
-
if (!existsSync(tmp)) mkdirSync(tmp);
|
|
11
|
+
if (!existsSync(tmp)) mkdirSync(tmp, { recursive: true });
|
|
12
12
|
|
|
13
13
|
export async function copyAsync(_path: string, fs: NodeFS = _fs): Promise<void> {
|
|
14
14
|
const path = relative(data, _path) || '/';
|
package/types/README.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
This directory contains declaration files. Due to numerous TypeScript shenanigans, placing them into `src` is a really bad idea. Instead, we put them here.
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
@types/readable-stream has *many* problems:
|
|
3
|
-
https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/71923
|
|
4
|
-
https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/71307
|
|
5
|
-
https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/71083
|
|
6
|
-
https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/66049
|
|
7
|
-
... and many more
|
|
8
|
-
|
|
9
|
-
This allows us to bypass those problems.
|
|
10
|
-
|
|
11
|
-
Warning: Do not install @types/readable-stream alongside this package!
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/// <reference types="node" preserve="true" />
|
|
15
|
-
declare module 'readable-stream' {
|
|
16
|
-
import { finished, Readable, Writable } from 'node:stream';
|
|
17
|
-
export { finished, Readable, Writable };
|
|
18
|
-
}
|