@zenfs/core 1.0.11 → 1.1.1

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/utils.d.ts CHANGED
@@ -17,20 +17,30 @@ export declare function mkdirpSync(path: string, mode: number, fs: FileSystem):
17
17
  * @hidden
18
18
  */
19
19
  export declare function levenshtein(a: string, b: string): number;
20
+ /** @hidden */
21
+ export declare const setImmediate: (callback: () => unknown) => void;
20
22
  /**
21
- * @hidden
23
+ * Encodes a string into a buffer
24
+ * @internal
22
25
  */
23
- export declare const setImmediate: (callback: () => unknown) => void;
26
+ export declare function encodeRaw(input: string): Uint8Array;
27
+ /**
28
+ * Decodes a string from a buffer
29
+ * @internal
30
+ */
31
+ export declare function decodeRaw(input?: Uint8Array): string;
24
32
  /**
25
33
  * Encodes a string into a buffer
26
34
  * @internal
27
35
  */
28
- export declare function encode(input: string): Uint8Array;
36
+ export declare function encodeUTF8(input: string): Uint8Array;
37
+ export { /** @deprecated @hidden */ encodeUTF8 as encode };
29
38
  /**
30
39
  * Decodes a string from a buffer
31
40
  * @internal
32
41
  */
33
- export declare function decode(input?: Uint8Array): string;
42
+ export declare function decodeUTF8(input?: Uint8Array): string;
43
+ export { /** @deprecated @hidden */ decodeUTF8 as decode };
34
44
  /**
35
45
  * Decodes a directory listing
36
46
  * @hidden
package/dist/utils.js CHANGED
@@ -83,45 +83,67 @@ export function levenshtein(a, b) {
83
83
  }
84
84
  return dd;
85
85
  }
86
+ /** @hidden */
87
+ export const setImmediate = typeof globalThis.setImmediate == 'function' ? globalThis.setImmediate : (cb) => setTimeout(cb, 0);
86
88
  /**
87
- * @hidden
89
+ * Encodes a string into a buffer
90
+ * @internal
88
91
  */
89
- export const setImmediate = typeof globalThis.setImmediate == 'function' ? globalThis.setImmediate : (cb) => setTimeout(cb, 0);
92
+ export function encodeRaw(input) {
93
+ if (typeof input != 'string') {
94
+ throw new ErrnoError(Errno.EINVAL, 'Can not encode a non-string');
95
+ }
96
+ return new Uint8Array(Array.from(input).map(char => char.charCodeAt(0)));
97
+ }
98
+ /**
99
+ * Decodes a string from a buffer
100
+ * @internal
101
+ */
102
+ export function decodeRaw(input) {
103
+ if (!(input instanceof Uint8Array)) {
104
+ throw new ErrnoError(Errno.EINVAL, 'Can not decode a non-Uint8Array');
105
+ }
106
+ return Array.from(input)
107
+ .map(char => String.fromCharCode(char))
108
+ .join('');
109
+ }
90
110
  const encoder = new TextEncoder();
91
111
  /**
92
112
  * Encodes a string into a buffer
93
113
  * @internal
94
114
  */
95
- export function encode(input) {
115
+ export function encodeUTF8(input) {
96
116
  if (typeof input != 'string') {
97
117
  throw new ErrnoError(Errno.EINVAL, 'Can not encode a non-string');
98
118
  }
99
119
  return encoder.encode(input);
100
120
  }
121
+ export { /** @deprecated @hidden */ encodeUTF8 as encode };
101
122
  const decoder = new TextDecoder();
102
123
  /**
103
124
  * Decodes a string from a buffer
104
125
  * @internal
105
126
  */
106
- export function decode(input) {
127
+ export function decodeUTF8(input) {
107
128
  if (!(input instanceof Uint8Array)) {
108
129
  throw new ErrnoError(Errno.EINVAL, 'Can not decode a non-Uint8Array');
109
130
  }
110
131
  return decoder.decode(input);
111
132
  }
133
+ export { /** @deprecated @hidden */ decodeUTF8 as decode };
112
134
  /**
113
135
  * Decodes a directory listing
114
136
  * @hidden
115
137
  */
116
138
  export function decodeDirListing(data) {
117
- return JSON.parse(decode(data), (k, v) => (k == '' ? v : BigInt(v)));
139
+ return JSON.parse(decodeUTF8(data), (k, v) => (k == '' ? v : BigInt(v)));
118
140
  }
119
141
  /**
120
142
  * Encodes a directory listing
121
143
  * @hidden
122
144
  */
123
145
  export function encodeDirListing(data) {
124
- return encode(JSON.stringify(data, (k, v) => (k == '' ? v : v.toString())));
146
+ return encodeUTF8(JSON.stringify(data, (k, v) => (k == '' ? v : v.toString())));
125
147
  }
126
148
  /**
127
149
  * converts Date or number to a integer UNIX timestamp
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "1.0.11",
3
+ "version": "1.1.1",
4
4
  "description": "A filesystem, anywhere",
5
+ "funding": {
6
+ "type": "individual",
7
+ "url": "https://github.com/sponsors/james-pre"
8
+ },
5
9
  "main": "dist/index.js",
6
10
  "types": "dist/index.d.ts",
7
11
  "keywords": [
package/readme.md CHANGED
@@ -15,9 +15,11 @@ ZenFS is modular and extensible. The core includes some built-in backends:
15
15
 
16
16
  ZenFS supports a number of other backends. Many are provided as separate packages under `@zenfs`. More backends can be defined by separate libraries by extending the `FileSystem` class and providing a `Backend` object.
17
17
 
18
+ You can find all of the packages available over at [zenfs.dev](https://zenfs.dev).
19
+
18
20
  As an added bonus, all ZenFS backends support synchronous operations. All of the backends included with the core are cross-platform.
19
21
 
20
- For more information, see the [docs](https://zen-fs.github.io/core).
22
+ For more information, see the [docs](https://zenfs.dev/core).
21
23
 
22
24
  ## Installing
23
25
 
@@ -143,8 +145,62 @@ fs.mount('/mnt/zip', zipfs);
143
145
  fs.umount('/mnt/zip'); // finished using the zip
144
146
  ```
145
147
 
148
+ > [!CAUTION]
149
+ > Instances of backends follow the _internal_ API. You should never use a backend's methods unless you are extending a backend.
150
+
151
+ #### Devices and device files
152
+
146
153
  > [!WARNING]
147
- > Instances of backends follow the **internal** ZenFS API. You should never use a backend's methods unless you are extending a backend.
154
+ > This is an **experimental** feature. Breaking changes may occur during non-major releases. Using this feature is the fastest way to make it stable.
155
+
156
+ ZenFS includes experimental support for device files. These are designed to follow Linux's device file behavior, for consistency and ease of use. You can automatically add some normal devices with the `addDevices` configuration option:
157
+
158
+ ```ts
159
+ await configure({
160
+ mounts: {
161
+ /* ... */
162
+ },
163
+ addDevices: true,
164
+ });
165
+
166
+ fs.writeFileSync('/dev/null', 'Some data to be discarded');
167
+
168
+ const randomData = new Unit8Array(100);
169
+
170
+ const random = fs.openSync('/dev/random', 'r');
171
+ fs.readSync(random, randomData);
172
+ fs.closeSync(random);
173
+ ```
174
+
175
+ You can create your own devices by implementing a `DeviceDriver`. For example, the null device looks similar to this:
176
+
177
+ ```ts
178
+ const customNullDevice = {
179
+ name: 'custom_null',
180
+ isBuffered: false,
181
+ read() {
182
+ return 0;
183
+ },
184
+ write() {},
185
+ };
186
+ ```
187
+
188
+ Note the actual implementation's write is slightly more complicated since it adds to the file position. You can find more information on the docs.
189
+
190
+ Finally, if you'd like to use your custom device with the file system, you can use so through the aptly named `DeviceFS`.
191
+
192
+ ```ts
193
+ const devfs = fs.mounts.get('/dev') as DeviceFS;
194
+ devfs.createDevice('/custom', customNullDevice);
195
+
196
+ fs.writeFileSync('/dev/custom', 'This gets discarded.');
197
+ ```
198
+
199
+ In the above example, `createDevice` works relative to the `DeviceFS` mount point.
200
+
201
+ Additionally, a type assertion (` as ...`) is used since `fs.mounts` does not keep track of which file system type is mapped to which mount point. Doing so would create significant maintenance costs due to the complexity of implementing it.
202
+
203
+ If you would like to see a more intuitive way adding custom devices (e.g. `fs.mknod`), please feel free to open an issue for a feature request.
148
204
 
149
205
  ## Using with bundlers
150
206
 
@@ -8,7 +8,7 @@ import { FileSystem } from '../filesystem.js';
8
8
  import { Readonly } from '../mixins/readonly.js';
9
9
  import type { StatsLike } from '../stats.js';
10
10
  import { Stats } from '../stats.js';
11
- import { decode, encode } from '../utils.js';
11
+ import { decodeUTF8, encodeUTF8 } from '../utils.js';
12
12
 
13
13
  /**
14
14
  * An Index in JSON form
@@ -83,7 +83,7 @@ export class Index extends Map<string, Stats> {
83
83
  for (const [path, data] of Object.entries(json.entries)) {
84
84
  const stats = new Stats(data);
85
85
  if (stats.isDirectory()) {
86
- stats.fileData = encode(JSON.stringify(this.dirEntries(path)));
86
+ stats.fileData = encodeUTF8(JSON.stringify(this.dirEntries(path)));
87
87
  }
88
88
  this.set(path, stats);
89
89
  }
@@ -195,7 +195,7 @@ export abstract class IndexFS extends Readonly(FileSystem) {
195
195
  throw ErrnoError.With('ENOTDIR', path, 'readdir');
196
196
  }
197
197
 
198
- const content: unknown = JSON.parse(decode(stats.fileData));
198
+ const content: unknown = JSON.parse(decodeUTF8(stats.fileData));
199
199
  if (!Array.isArray(content)) {
200
200
  throw ErrnoError.With('ENODATA', path, 'readdir');
201
201
  }
@@ -6,7 +6,7 @@ import type { FileSystemMetadata } from '../filesystem.js';
6
6
  import { FileSystem } from '../filesystem.js';
7
7
  import { Mutexed } from '../mixins/mutexed.js';
8
8
  import { Stats } from '../stats.js';
9
- import { decode, encode } from '../utils.js';
9
+ import { decodeUTF8, encodeUTF8 } from '../utils.js';
10
10
  import type { Backend } from './backend.js';
11
11
  /** @internal */
12
12
  const deletionLogPath = '/.deleted';
@@ -99,7 +99,7 @@ export class UnmutexedOverlayFS extends FileSystem {
99
99
  const file = await this.writable.openFile(deletionLogPath, parseFlag('r'));
100
100
  const { size } = await file.stat();
101
101
  const { buffer } = await file.read(new Uint8Array(size));
102
- this._deleteLog = decode(buffer);
102
+ this._deleteLog = decodeUTF8(buffer);
103
103
  } catch (err) {
104
104
  if ((err as ErrnoError).errno !== Errno.ENOENT) {
105
105
  throw err;
@@ -386,7 +386,7 @@ export class UnmutexedOverlayFS extends FileSystem {
386
386
  this._deleteLogUpdatePending = true;
387
387
  const log = await this.writable.openFile(deletionLogPath, parseFlag('w'));
388
388
  try {
389
- await log.write(encode(this._deleteLog));
389
+ await log.write(encodeUTF8(this._deleteLog));
390
390
  if (this._deleteLogUpdateNeeded) {
391
391
  this._deleteLogUpdateNeeded = false;
392
392
  await this.updateLog('');
@@ -6,8 +6,9 @@ import { PreloadFile } from '../../file.js';
6
6
  import { FileSystem, type FileSystemMetadata } from '../../filesystem.js';
7
7
  import { type Ino, Inode, randomIno, rootIno } from '../../inode.js';
8
8
  import type { FileType, Stats } from '../../stats.js';
9
- import { decodeDirListing, encode, encodeDirListing } from '../../utils.js';
9
+ import { decodeDirListing, encodeUTF8, encodeDirListing } from '../../utils.js';
10
10
  import type { Store, Transaction } from './store.js';
11
+ import type { File } from '../../file.js';
11
12
 
12
13
  const maxInodeAllocTries = 5;
13
14
 
@@ -172,17 +173,17 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
172
173
  return this.findINodeSync(tx, path).toStats();
173
174
  }
174
175
 
175
- public async createFile(path: string, flag: string, mode: number): Promise<PreloadFile<this>> {
176
+ public async createFile(path: string, flag: string, mode: number): Promise<File> {
176
177
  const node = await this.commitNew(path, S_IFREG, mode, new Uint8Array(0));
177
178
  return new PreloadFile(this, path, flag, node.toStats(), new Uint8Array(0));
178
179
  }
179
180
 
180
- public createFileSync(path: string, flag: string, mode: number): PreloadFile<this> {
181
+ public createFileSync(path: string, flag: string, mode: number): File {
181
182
  this.commitNewSync(path, S_IFREG, mode);
182
183
  return this.openFileSync(path, flag);
183
184
  }
184
185
 
185
- public async openFile(path: string, flag: string): Promise<PreloadFile<this>> {
186
+ public async openFile(path: string, flag: string): Promise<File> {
186
187
  await using tx = this.store.transaction();
187
188
  const node = await this.findINode(tx, path),
188
189
  data = await tx.get(node.ino);
@@ -192,7 +193,7 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
192
193
  return new PreloadFile(this, path, flag, node.toStats(), data);
193
194
  }
194
195
 
195
- public openFileSync(path: string, flag: string): PreloadFile<this> {
196
+ public openFileSync(path: string, flag: string): File {
196
197
  using tx = this.store.transaction();
197
198
  const node = this.findINodeSync(tx, path),
198
199
  data = tx.getSync(node.ino);
@@ -225,11 +226,11 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
225
226
  }
226
227
 
227
228
  public async mkdir(path: string, mode: number): Promise<void> {
228
- await this.commitNew(path, S_IFDIR, mode, encode('{}'));
229
+ await this.commitNew(path, S_IFDIR, mode, encodeUTF8('{}'));
229
230
  }
230
231
 
231
232
  public mkdirSync(path: string, mode: number): void {
232
- this.commitNewSync(path, S_IFDIR, mode, encode('{}'));
233
+ this.commitNewSync(path, S_IFDIR, mode, encodeUTF8('{}'));
233
234
  }
234
235
 
235
236
  public async readdir(path: string): Promise<string[]> {
@@ -334,7 +335,7 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
334
335
  const inode = new Inode();
335
336
  inode.mode = 0o777 | S_IFDIR;
336
337
  // If the root doesn't exist, the first random ID shouldn't exist either.
337
- await tx.set(inode.ino, encode('{}'));
338
+ await tx.set(inode.ino, encodeUTF8('{}'));
338
339
  await tx.set(rootIno, inode.data);
339
340
  await tx.commit();
340
341
  }
@@ -351,7 +352,7 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
351
352
  const inode = new Inode();
352
353
  inode.mode = 0o777 | S_IFDIR;
353
354
  // If the root doesn't exist, the first random ID shouldn't exist either.
354
- tx.setSync(inode.ino, encode('{}'));
355
+ tx.setSync(inode.ino, encodeUTF8('{}'));
355
356
  tx.setSync(rootIno, inode.data);
356
357
  tx.commitSync();
357
358
  }
package/src/config.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Backend, BackendConfiguration, FilesystemOf, SharedConfig } from './backends/backend.js';
2
2
  import { checkOptions, isBackend, isBackendConfig } from './backends/backend.js';
3
3
  import { credentials } from './credentials.js';
4
+ import { DeviceFS, fullDevice, nullDevice, randomDevice, zeroDevice } from './devices.js';
4
5
  import * as fs from './emulation/index.js';
5
6
  import type { AbsolutePath } from './emulation/path.js';
6
7
  import type { MountObject } from './emulation/shared.js';
@@ -77,14 +78,25 @@ export interface Configuration<T extends ConfigMounts> extends SharedConfig {
77
78
  * An object mapping mount points to mount configuration
78
79
  */
79
80
  mounts: { [K in keyof T & AbsolutePath]: MountConfiguration<T[K]> };
81
+
80
82
  /**
81
83
  * The uid to use
84
+ * @default 0
82
85
  */
83
86
  uid: number;
87
+
84
88
  /**
85
89
  * The gid to use
90
+ * @default 0
86
91
  */
87
92
  gid: number;
93
+
94
+ /**
95
+ * Whether to automatically add normal Linux devices
96
+ * @default false
97
+ * @experimental
98
+ */
99
+ addDevices: boolean;
88
100
  }
89
101
 
90
102
  /**
@@ -110,6 +122,16 @@ export async function configure<T extends ConfigMounts>(configuration: Partial<C
110
122
 
111
123
  Object.assign(credentials, { uid, gid, suid: uid, sgid: gid, euid: uid, egid: gid });
112
124
 
125
+ if (configuration.addDevices) {
126
+ const devfs = new DeviceFS();
127
+ devfs.createDevice('/null', nullDevice);
128
+ devfs.createDevice('/zero', zeroDevice);
129
+ devfs.createDevice('/full', fullDevice);
130
+ devfs.createDevice('/random', randomDevice);
131
+ await devfs.ready();
132
+ fs.mount('/dev', devfs);
133
+ }
134
+
113
135
  if (!configuration.mounts) {
114
136
  return;
115
137
  }