@zenfs/core 0.5.3 → 0.5.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.
@@ -214,10 +214,9 @@ export declare function lopen(path: PathLike, flag: string, mode?: Node.Mode): P
214
214
  export declare function readFile(filename: PathLike, options?: {
215
215
  flag?: Node.OpenMode;
216
216
  }): Promise<Uint8Array>;
217
- export declare function readFile(filename: PathLike, options: {
218
- encoding?: BufferEncoding;
217
+ export declare function readFile(filename: PathLike, options: (Node.BaseEncodingOptions & {
219
218
  flag?: Node.OpenMode;
220
- } | BufferEncoding): Promise<string>;
219
+ }) | BufferEncoding): Promise<string>;
221
220
  /**
222
221
  * Synchronously writes data to a file, replacing the file if it already exists.
223
222
  *
@@ -6,6 +6,7 @@ import { BigIntStats, FileType } from '../stats.js';
6
6
  import { decode, encode } from '../utils.js';
7
7
  import { Dirent } from './dir.js';
8
8
  import { dirname, join } from './path.js';
9
+ import { F_OK } from './constants.js';
9
10
  export class FileHandle {
10
11
  constructor(
11
12
  /**
@@ -98,7 +99,7 @@ export class FileHandle {
98
99
  return fd2file(this.fd).read(buffer, offset, length, position);
99
100
  }
100
101
  async readFile(_options) {
101
- const options = normalizeOptions(_options, null, 'r', null);
102
+ const options = normalizeOptions(_options, null, 'r', 0o444);
102
103
  const flag = parseFlag(options.flag);
103
104
  if (!isReadable(flag)) {
104
105
  throw new ApiError(ErrorCode.EINVAL, 'Flag passed must allow for reading.');
@@ -208,15 +209,18 @@ async function doOp(...[name, resolveSymlinks, rawPath, ...args]) {
208
209
  export async function rename(oldPath, newPath) {
209
210
  oldPath = normalizePath(oldPath);
210
211
  newPath = normalizePath(newPath);
211
- const { path: old } = resolveFS(oldPath);
212
- const { fs, path } = resolveFS(newPath);
212
+ const src = resolveFS(oldPath);
213
+ const dst = resolveFS(newPath);
213
214
  try {
214
- const data = await readFile(oldPath);
215
- await writeFile(newPath, data);
215
+ if (src.mountPoint == dst.mountPoint) {
216
+ await src.fs.rename(src.path, dst.path, cred);
217
+ return;
218
+ }
219
+ await writeFile(newPath, await readFile(oldPath));
216
220
  await unlink(oldPath);
217
221
  }
218
222
  catch (e) {
219
- throw fixError(e, { [old]: oldPath, [path]: newPath });
223
+ throw fixError(e, { [src.path]: oldPath, [dst.path]: newPath });
220
224
  }
221
225
  }
222
226
  rename;
@@ -279,7 +283,7 @@ async function _open(_path, _flag, _mode = 0o644, resolveSymlinks) {
279
283
  try {
280
284
  switch (pathExistsAction(flag)) {
281
285
  case ActionType.THROW:
282
- throw ApiError.EEXIST(path);
286
+ throw ApiError.With('EEXIST', path, '_open');
283
287
  case ActionType.TRUNCATE:
284
288
  /*
285
289
  In a previous implementation, we deleted the file and
@@ -307,11 +311,11 @@ async function _open(_path, _flag, _mode = 0o644, resolveSymlinks) {
307
311
  // Ensure parent exists.
308
312
  const parentStats = await doOp('stat', resolveSymlinks, dirname(path), cred);
309
313
  if (parentStats && !parentStats.isDirectory()) {
310
- throw ApiError.ENOTDIR(dirname(path));
314
+ throw ApiError.With('ENOTDIR', dirname(path), '_open');
311
315
  }
312
316
  return await doOp('createFile', resolveSymlinks, path, flag, mode, cred);
313
317
  case ActionType.THROW:
314
- throw ApiError.ENOENT(path);
318
+ throw ApiError.With('ENOENT', path, '_open');
315
319
  default:
316
320
  throw new ApiError(ErrorCode.EINVAL, 'Invalid file flag');
317
321
  }
@@ -527,7 +531,7 @@ export async function symlink(target, path, type = 'file') {
527
531
  throw new ApiError(ErrorCode.EINVAL, 'Invalid symlink type: ' + type);
528
532
  }
529
533
  if (await exists(path)) {
530
- throw ApiError.EEXIST(path);
534
+ throw ApiError.With('EEXIST', path, 'symlink');
531
535
  }
532
536
  await writeFile(path, target);
533
537
  const file = await _open(path, 'r+', 0o644, false);
@@ -668,7 +672,7 @@ export async function watch(filename, arg2, listener = nop) {
668
672
  * @param path
669
673
  * @param mode
670
674
  */
671
- export async function access(path, mode = 0o600) {
675
+ export async function access(path, mode = F_OK) {
672
676
  const stats = await stat(path);
673
677
  if (!stats.hasAccess(mode, cred)) {
674
678
  throw new ApiError(ErrorCode.EACCES);
@@ -1,7 +1,9 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
+ /// <reference types="node" resolution-mode="require"/>
2
3
  import { Cred } from '../cred.js';
3
4
  import { FileSystem } from '../filesystem.js';
4
5
  import type { File } from '../file.js';
6
+ import type { BaseEncodingOptions, OpenMode, WriteFileOptions } from 'node:fs';
5
7
  /**
6
8
  * converts Date or number to a integer UNIX timestamp
7
9
  * Grabbed from NodeJS sources (lib/fs.js)
@@ -26,9 +28,15 @@ export declare function normalizeTime(time: string | number | Date): Date;
26
28
  export declare function normalizePath(p: string): string;
27
29
  /**
28
30
  * Normalizes options
31
+ * @param options options to normalize
32
+ * @param encoding default encoding
33
+ * @param flag default flag
34
+ * @param mode default mode
29
35
  * @internal
30
36
  */
31
- export declare function normalizeOptions(options: unknown, defEnc: string | null, defFlag: string, defMode: number | null): {
37
+ export declare function normalizeOptions(options?: WriteFileOptions | (BaseEncodingOptions & {
38
+ flag?: OpenMode;
39
+ }), encoding?: BufferEncoding, flag?: string, mode?: number): {
32
40
  encoding: BufferEncoding;
33
41
  flag: string;
34
42
  mode: number;
@@ -23,16 +23,14 @@ export function _toUnixTimestamp(time) {
23
23
  * @internal
24
24
  */
25
25
  export function normalizeMode(mode, def) {
26
- switch (typeof mode) {
27
- case 'number':
28
- // (path, flag, mode, cb?)
29
- return mode;
30
- case 'string':
31
- // (path, flag, modeString, cb?)
32
- const trueMode = parseInt(mode, 8);
33
- if (!isNaN(trueMode)) {
34
- return trueMode;
35
- }
26
+ if (typeof mode == 'number') {
27
+ return mode;
28
+ }
29
+ if (typeof mode == 'string') {
30
+ const parsed = parseInt(mode, 8);
31
+ if (!isNaN(parsed)) {
32
+ return parsed;
33
+ }
36
34
  }
37
35
  if (typeof def == 'number') {
38
36
  return def;
@@ -61,45 +59,35 @@ export function normalizeTime(time) {
61
59
  */
62
60
  export function normalizePath(p) {
63
61
  // Node doesn't allow null characters in paths.
64
- if (p.indexOf('\u0000') >= 0) {
62
+ if (p.includes('\x00')) {
65
63
  throw new ApiError(ErrorCode.EINVAL, 'Path must be a string without null bytes.');
66
64
  }
67
- if (p === '') {
65
+ if (p.length == 0) {
68
66
  throw new ApiError(ErrorCode.EINVAL, 'Path must not be empty.');
69
67
  }
70
- p = p.replaceAll(/\/+/g, '/');
71
- return resolve(p);
68
+ return resolve(p.replaceAll(/[/\\]+/g, '/'));
72
69
  }
73
70
  /**
74
71
  * Normalizes options
72
+ * @param options options to normalize
73
+ * @param encoding default encoding
74
+ * @param flag default flag
75
+ * @param mode default mode
75
76
  * @internal
76
77
  */
77
- export function normalizeOptions(options, defEnc, defFlag, defMode) {
78
- // typeof null === 'object' so special-case handing is needed.
79
- switch (options === null ? 'null' : typeof options) {
80
- case 'object':
81
- return {
82
- encoding: typeof options['encoding'] !== 'undefined' ? options['encoding'] : defEnc,
83
- flag: typeof options['flag'] !== 'undefined' ? options['flag'] : defFlag,
84
- mode: normalizeMode(options['mode'], defMode),
85
- };
86
- case 'string':
87
- return {
88
- encoding: options,
89
- flag: defFlag,
90
- mode: defMode,
91
- };
92
- case 'null':
93
- case 'undefined':
94
- case 'function':
95
- return {
96
- encoding: defEnc,
97
- flag: defFlag,
98
- mode: defMode,
99
- };
100
- default:
101
- throw new TypeError(`"options" must be a string or an object, got ${typeof options} instead.`);
102
- }
78
+ export function normalizeOptions(options, encoding = 'utf8', flag, mode = 0) {
79
+ if (typeof options != 'object' || options === null) {
80
+ return {
81
+ encoding: typeof options == 'string' ? options : encoding,
82
+ flag,
83
+ mode,
84
+ };
85
+ }
86
+ return {
87
+ encoding: typeof options?.encoding == 'string' ? options.encoding : encoding,
88
+ flag: typeof options?.flag == 'string' ? options.flag : flag,
89
+ mode: normalizeMode('mode' in options ? options?.mode : null, mode),
90
+ };
103
91
  }
104
92
  /**
105
93
  * Do nothing
@@ -76,11 +76,9 @@ export declare function lopenSync(path: PathLike, flag: string, mode?: Node.Mode
76
76
  export declare function readFileSync(filename: string, options?: {
77
77
  flag?: string;
78
78
  }): Uint8Array;
79
- export declare function readFileSync(filename: string, options: {
80
- encoding: string;
79
+ export declare function readFileSync(filename: string, options: (Node.BaseEncodingOptions & {
81
80
  flag?: string;
82
- }): string;
83
- export declare function readFileSync(filename: string, encoding: string): string;
81
+ }) | BufferEncoding): string;
84
82
  /**
85
83
  * Synchronously writes data to a file, replacing the file if it already
86
84
  * exists.
@@ -106,8 +104,7 @@ export declare function writeFileSync(filename: string, data: FileContents, enco
106
104
  * @option options mode Defaults to `0644`.
107
105
  * @option options flag Defaults to `'a'`.
108
106
  */
109
- export declare function appendFileSync(filename: string, data: FileContents, options?: Node.WriteFileOptions): void;
110
- export declare function appendFileSync(filename: string, data: FileContents, encoding?: string): void;
107
+ export declare function appendFileSync(filename: string, data: FileContents, arg3?: Node.WriteFileOptions): void;
111
108
  /**
112
109
  * Synchronous `fstat`.
113
110
  * `fstat()` is identical to `stat()`, except that the file to be stat-ed is
@@ -31,8 +31,7 @@ export function renameSync(oldPath, newPath) {
31
31
  if (_old === _new) {
32
32
  return _old.fs.renameSync(_old.path, _new.path, cred);
33
33
  }
34
- const data = readFileSync(oldPath);
35
- writeFileSync(newPath, data);
34
+ writeFileSync(newPath, readFileSync(oldPath));
36
35
  unlinkSync(oldPath);
37
36
  }
38
37
  catch (e) {
@@ -105,22 +104,22 @@ function _openSync(_path, _flag, _mode, resolveSymlinks) {
105
104
  // Ensure parent exists.
106
105
  const parentStats = doOp('statSync', resolveSymlinks, dirname(path), cred);
107
106
  if (!parentStats.isDirectory()) {
108
- throw ApiError.ENOTDIR(dirname(path));
107
+ throw ApiError.With('ENOTDIR', dirname(path), '_openSync');
109
108
  }
110
109
  return doOp('createFileSync', resolveSymlinks, path, flag, mode, cred);
111
110
  case ActionType.THROW:
112
- throw ApiError.ENOENT(path);
111
+ throw ApiError.With('ENOENT', path, '_openSync');
113
112
  default:
114
113
  throw new ApiError(ErrorCode.EINVAL, 'Invalid FileFlag object.');
115
114
  }
116
115
  }
117
116
  if (!stats.hasAccess(mode, cred)) {
118
- throw ApiError.EACCES(path);
117
+ throw ApiError.With('EACCES', path, '_openSync');
119
118
  }
120
119
  // File exists.
121
120
  switch (pathExistsAction(flag)) {
122
121
  case ActionType.THROW:
123
- throw ApiError.EEXIST(path);
122
+ throw ApiError.With('EEXIST', path, '_openSync');
124
123
  case ActionType.TRUNCATE:
125
124
  // Delete file.
126
125
  doOp('unlinkSync', resolveSymlinks, path, cred);
@@ -228,6 +227,17 @@ function _appendFileSync(fname, data, flag, mode, resolveSymlinks) {
228
227
  file.closeSync();
229
228
  }
230
229
  }
230
+ /**
231
+ * Asynchronously append data to a file, creating the file if it not yet
232
+ * exists.
233
+ *
234
+ * @param filename
235
+ * @param data
236
+ * @param options
237
+ * @option options encoding Defaults to `'utf8'`.
238
+ * @option options mode Defaults to `0644`.
239
+ * @option options flag Defaults to `'a'`.
240
+ */
231
241
  export function appendFileSync(filename, data, arg3) {
232
242
  const options = normalizeOptions(arg3, 'utf8', 'a', 0o644);
233
243
  const flag = parseFlag(options.flag);
@@ -414,7 +424,7 @@ export function symlinkSync(target, path, type = 'file') {
414
424
  throw new ApiError(ErrorCode.EINVAL, 'Invalid type: ' + type);
415
425
  }
416
426
  if (existsSync(path)) {
417
- throw ApiError.EEXIST(path);
427
+ throw ApiError.With('EEXIST', path, 'symlinkSync');
418
428
  }
419
429
  writeFileSync(path, target);
420
430
  const file = _openSync(path, 'r+', 0o644, false);
package/dist/file.d.ts CHANGED
@@ -29,6 +29,10 @@ export declare enum ActionType {
29
29
  export declare function parseFlag(flag: string | number): string;
30
30
  export declare function flagToString(flag: number): string;
31
31
  export declare function flagToNumber(flag: string): number;
32
+ /**
33
+ * Parses a flag as a mode (W_OK, R_OK, and/or X_OK)
34
+ * @param flag the flag to parse
35
+ */
32
36
  export declare function flagToMode(flag: string): number;
33
37
  export declare function isReadable(flag: string): boolean;
34
38
  export declare function isWriteable(flag: string): boolean;
package/dist/file.js CHANGED
@@ -86,6 +86,10 @@ export function flagToNumber(flag) {
86
86
  throw new Error('Invalid flag string: ' + flag);
87
87
  }
88
88
  }
89
+ /**
90
+ * Parses a flag as a mode (W_OK, R_OK, and/or X_OK)
91
+ * @param flag the flag to parse
92
+ */
89
93
  export function flagToMode(flag) {
90
94
  let mode = 0;
91
95
  mode <<= 1;
@@ -323,15 +327,16 @@ export class PreloadFile extends File {
323
327
  }
324
328
  }
325
329
  }
326
- this._buffer.set(buffer.slice(offset, offset + length), position);
327
- const len = this._buffer.byteOffset;
330
+ const slice = buffer.slice(offset, offset + length);
331
+ this._buffer.set(slice, position);
332
+ const bytesWritten = slice.byteLength;
328
333
  this.stats.mtimeMs = Date.now();
329
334
  if (isSynchronous(this.flag)) {
330
335
  this.syncSync();
331
- return len;
336
+ return bytesWritten;
332
337
  }
333
- this.position = position + len;
334
- return len;
338
+ this.position = position + bytesWritten;
339
+ return bytesWritten;
335
340
  }
336
341
  /**
337
342
  * Read data from the file.
@@ -389,9 +394,6 @@ export class PreloadFile extends File {
389
394
  * @param mode
390
395
  */
391
396
  chmodSync(mode) {
392
- if (!this.fs.metadata().supportsProperties) {
393
- throw new ApiError(ErrorCode.ENOTSUP);
394
- }
395
397
  this._dirty = true;
396
398
  this.stats.chmod(mode);
397
399
  this.syncSync();
@@ -410,9 +412,6 @@ export class PreloadFile extends File {
410
412
  * @param gid
411
413
  */
412
414
  chownSync(uid, gid) {
413
- if (!this.fs.metadata().supportsProperties) {
414
- throw new ApiError(ErrorCode.ENOTSUP);
415
- }
416
415
  this._dirty = true;
417
416
  this.stats.chown(uid, gid);
418
417
  this.syncSync();
@@ -421,9 +420,6 @@ export class PreloadFile extends File {
421
420
  this.utimesSync(atime, mtime);
422
421
  }
423
422
  utimesSync(atime, mtime) {
424
- if (!this.fs.metadata().supportsProperties) {
425
- throw new ApiError(ErrorCode.ENOTSUP);
426
- }
427
423
  this._dirty = true;
428
424
  this.stats.atime = atime;
429
425
  this.stats.mtime = mtime;
package/dist/stats.d.ts CHANGED
@@ -146,9 +146,8 @@ export declare abstract class StatsCommon<T extends number | bigint> implements
146
146
  isFIFO(): boolean;
147
147
  /**
148
148
  * Checks if a given user/group has access to this item
149
- * @param mode The request access as 4 bits (unused, read, write, execute)
150
- * @param uid The requesting UID
151
- * @param gid The requesting GID
149
+ * @param mode The requested access, combination of W_OK, R_OK, and X_OK
150
+ * @param cred The requesting credentials
152
151
  * @returns True if the request has access, false if the request does not
153
152
  * @internal
154
153
  */
package/dist/stats.js CHANGED
@@ -1,4 +1,4 @@
1
- import { S_IFDIR, S_IFLNK, S_IFMT, S_IFREG } from './emulation/constants.js';
1
+ import { S_IFDIR, S_IFLNK, S_IFMT, S_IFREG, S_IRWXG, S_IRWXO, S_IRWXU } from './emulation/constants.js';
2
2
  /**
3
3
  * Indicates the type of the given file. Applied to 'mode'.
4
4
  */
@@ -145,9 +145,8 @@ export class StatsCommon {
145
145
  }
146
146
  /**
147
147
  * Checks if a given user/group has access to this item
148
- * @param mode The request access as 4 bits (unused, read, write, execute)
149
- * @param uid The requesting UID
150
- * @param gid The requesting GID
148
+ * @param mode The requested access, combination of W_OK, R_OK, and X_OK
149
+ * @param cred The requesting credentials
151
150
  * @returns True if the request has access, false if the request does not
152
151
  * @internal
153
152
  */
@@ -156,24 +155,9 @@ export class StatsCommon {
156
155
  //Running as root
157
156
  return true;
158
157
  }
159
- const perms = this.mode & ~S_IFMT;
160
- let uMode = 0xf, gMode = 0xf, wMode = 0xf;
161
- if (cred.euid == this.uid) {
162
- const uPerms = (0xf00 & perms) >> 8;
163
- uMode = (mode ^ uPerms) & mode;
164
- }
165
- if (cred.egid == this.gid) {
166
- const gPerms = (0xf0 & perms) >> 4;
167
- gMode = (mode ^ gPerms) & mode;
168
- }
169
- const wPerms = 0xf & perms;
170
- wMode = (mode ^ wPerms) & mode;
171
- /*
172
- Result = 0b0xxx (read, write, execute)
173
- If any bits are set that means the request does not have that permission.
174
- */
175
- const result = uMode & gMode & wMode;
176
- return !result;
158
+ // Mask for
159
+ const adjusted = (cred.uid == this.uid ? S_IRWXU : 0) | (cred.gid == this.gid ? S_IRWXG : 0) | S_IRWXO;
160
+ return (mode & this.mode & adjusted) == mode;
177
161
  }
178
162
  /**
179
163
  * Convert the current stats object into a credentials object
package/dist/utils.js CHANGED
@@ -103,6 +103,7 @@ export function encode(input, encoding = 'utf8') {
103
103
  }
104
104
  switch (encoding) {
105
105
  case 'ascii':
106
+ return new Uint8Array(Array.from(input).map(char => char.charCodeAt(0) & 0x7f));
106
107
  case 'latin1':
107
108
  case 'binary':
108
109
  return new Uint8Array(Array.from(input).map(char => char.charCodeAt(0)));
@@ -124,7 +125,7 @@ export function encode(input, encoding = 'utf8') {
124
125
  return [(code >> 18) | 0xf0, ((code >> 12) & 0x3f) | 0x80, b, a];
125
126
  }));
126
127
  case 'base64':
127
- return encode(atob(input), 'utf-8');
128
+ return encode(atob(input), 'binary');
128
129
  case 'base64url':
129
130
  return encode(input.replace('_', '/').replace('-', '+'), 'base64');
130
131
  case 'hex':
@@ -151,6 +152,9 @@ export function decode(input, encoding = 'utf8') {
151
152
  }
152
153
  switch (encoding) {
153
154
  case 'ascii':
155
+ return Array.from(input)
156
+ .map(char => String.fromCharCode(char & 0x7f))
157
+ .join('');
154
158
  case 'latin1':
155
159
  case 'binary':
156
160
  return Array.from(input)
@@ -186,7 +190,7 @@ export function decode(input, encoding = 'utf8') {
186
190
  }
187
191
  return utf16leString;
188
192
  case 'base64':
189
- return btoa(decode(input, 'utf-8'));
193
+ return btoa(decode(input, 'binary'));
190
194
  case 'base64url':
191
195
  return decode(input, 'base64').replace('/', '_').replace('+', '-');
192
196
  case 'hex':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "A filesystem in your browser",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist",
package/readme.md CHANGED
@@ -17,7 +17,7 @@ ZenFS is modular and extensible. The core includes a few built-in backends:
17
17
  > [!NOTE]
18
18
  > When constructed, `AsyncMirror` loads the entire contents of the async file system into a synchronous backend. It performs operations on the synchronous file system and then queues them to be mirrored onto the asynchronous backend.
19
19
 
20
- ZenFS supports a number of other backends. Many are provided as seperate packages under `@zenfs`. More backends can be defined by separate libraries by extending the `FileSystem` class and/or providing a `Backend` object.
20
+ 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/or providing a `Backend` object.
21
21
 
22
22
  For more information, see the [docs](https://zen-fs.github.io/core).
23
23
 
@@ -110,7 +110,7 @@ if (!exists) {
110
110
  > You can import the promises API using `promises`, or using `fs.promises` on the exported `fs`.
111
111
 
112
112
  > [!IMPORTANT]
113
- > ZenFS does _not_ provide a seperate public import for importing promises like `fs/promises`. If you are using ESM, you can import promises functions like `fs/promises` from the `dist/emulation/promises.ts` file, though this may change at any time and is **not recommended**.
113
+ > ZenFS does _not_ provide a separate public import for importing promises like `fs/promises`. If you are using ESM, you can import promises functions like `fs/promises` from the `dist/emulation/promises.ts` file, though this may change at any time and is **not recommended**.
114
114
 
115
115
  #### Using asynchronous backends synchronously
116
116