@zenfs/core 2.4.2 → 2.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/COPYING.md CHANGED
@@ -1,18 +1,24 @@
1
1
  _This document is supplemental to the license._
2
2
 
3
- **This is a very easy requirement, please respect it.**
4
- Feel free to reach out if you have any concerns regarding licensing.
3
+ Feel free to reach out if you have any concerns regarding licensing. You can create a new discussion [here](https://github.com/orgs/zen-fs/discussions/new?category=q-a).
4
+
5
+ Regarding section 4(b) of version 3 of the LGPL, you are permitted to link to a copy of the LGPL and this document rather than including the full text of both.
6
+
7
+ #### Exception for web applications
5
8
 
6
- If you convey copies of ZenFS (including in bundles), you must meet section 4 and 5 of the license. This includes at a minimum:
9
+ You are permitted to convey the Combined Work without meeting the requirements of section 4(d) of version 3 of the LGPL if
7
10
 
8
- - disclosure that ZenFS is in use
9
- - linking to the GitHub or npm pages
10
- - referencing the license and copyright.
11
+ - a) the Combined Work is only accessed by users from other computers over a network (i.e. a web application or website), and
12
+ - b) you also provide the [Corresponding Source](https://www.gnu.org/licenses/gpl-3.0.html#:~:text=“Corresponding%20Source”) of The Library or an offer thereof.
13
+
14
+ This exception does not excuse or waive any other requirement of the LGPL.
15
+
16
+ The practical goal of this exception is to allow ZenFS to be used in modern web apps. For the avoidance of doubt, you are permitted to bundle and minify ZenFS in a web application. Linking to ZenFS' GitHub or npm page is sufficient to satisfy the above section (b) provided you do not modify ZenFS.
11
17
 
12
18
  For example, in a list of libraries you could have:
13
19
 
14
- > - [ZenFS](https://github.com/zen-fs/core), LGPL-3.0-or-later, Copyright © James Prevett and other ZenFS contributors
20
+ > - [ZenFS](https://github.com/zen-fs/core), Licensed under the [LGPL 3.0 or later](https://www.gnu.org/licenses/lgpl-3.0.html) and [COPYING.md](https://github.com/zen-fs/core/blob/main/COPYING.md), Copyright © James Prevett and other ZenFS contributors
15
21
 
16
- <br />
22
+ **This is a very easy requirement, please respect it.**
17
23
 
18
24
  Before v2.4.0, ZenFS was licensed under the MIT. This still means you need to include the copyright notice and license.
@@ -9,7 +9,7 @@ type OptionType = 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undef
9
9
  * @category Backends and Configuration
10
10
  */
11
11
  export type OptionsConfig<T> = {
12
- [K in keyof T]: {
12
+ [K in Exclude<keyof T, keyof SharedConfig>]: {
13
13
  /**
14
14
  * The type of the option. Can be a:
15
15
  * - string given by `typeof`
@@ -26,6 +26,7 @@ declare const MetadataBlock_base: import("memium/decorators").StructFromTypedArr
26
26
  */
27
27
  export declare class MetadataBlock extends MetadataBlock_base<ArrayBufferLike> {
28
28
  readonly ['constructor']: typeof MetadataBlock;
29
+ private static readonly lockIndex;
29
30
  /**
30
31
  * The crc32c checksum for the metadata block.
31
32
  * @privateRemarks Keep this first!
@@ -37,7 +38,6 @@ export declare class MetadataBlock extends MetadataBlock_base<ArrayBufferLike> {
37
38
  accessor previous_offset: number;
38
39
  protected _previous?: MetadataBlock;
39
40
  get previous(): MetadataBlock | undefined;
40
- protected get offsetHex(): string;
41
41
  /** Metadata entries. */
42
42
  accessor items: ArrayOf<MetadataEntry>;
43
43
  toString(long?: boolean): string;
@@ -86,7 +86,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
86
86
  });
87
87
  // SPDX-License-Identifier: LGPL-3.0-or-later
88
88
  import { withErrno } from 'kerium';
89
- import { alert, crit, err, warn } from 'kerium/log';
89
+ import { alert, crit, debug, err, warn } from 'kerium/log';
90
90
  import { array, offsetof, sizeof } from 'memium';
91
91
  import { $from, field, struct, types as t } from 'memium/decorators';
92
92
  import { BufferView } from 'utilium/buffer.js';
@@ -95,6 +95,7 @@ import { decodeUUID, encodeUUID } from 'utilium/string.js';
95
95
  import { _inode_version, Inode } from '../internal/inode.js';
96
96
  import { StoreFS } from './store/fs.js';
97
97
  import { SyncMapTransaction } from './store/map.js';
98
+ const hex = (value) => '0x' + value.toString(16).padStart(8, '0');
98
99
  // eslint-disable-next-line @typescript-eslint/unbound-method
99
100
  const { format } = new Intl.NumberFormat('en-US', {
100
101
  notation: 'compact',
@@ -155,7 +156,7 @@ let MetadataEntry = (() => {
155
156
  get size() { return this.#size_accessor_storage; }
156
157
  set size(value) { this.#size_accessor_storage = value; }
157
158
  toString() {
158
- return `<MetadataEntry @ 0x${this.byteOffset.toString(16).padStart(8, '0')}>`;
159
+ return `<MetadataEntry @ ${hex(this.byteOffset)}>`;
159
160
  }
160
161
  constructor() {
161
162
  super(...arguments);
@@ -216,8 +217,8 @@ let MetadataBlock = (() => {
216
217
  __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
217
218
  MetadataBlock = _classThis = _classDescriptor.value;
218
219
  if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
219
- __runInitializers(_classThis, _classExtraInitializers);
220
220
  }
221
+ static lockIndex = offsetof(MetadataBlock, 'locked') / Int32Array.BYTES_PER_ELEMENT;
221
222
  #checksum_accessor_storage = __runInitializers(this, _checksum_initializers, void 0);
222
223
  /**
223
224
  * The crc32c checksum for the metadata block.
@@ -240,27 +241,24 @@ let MetadataBlock = (() => {
240
241
  this._previous ??= new MetadataBlock(this.buffer, this.previous_offset);
241
242
  return this._previous;
242
243
  }
243
- get offsetHex() {
244
- return '0x' + this.byteOffset.toString(16).padStart(8, '0');
245
- }
246
244
  #items_accessor_storage = __runInitializers(this, _items_initializers, void 0);
247
245
  /** Metadata entries. */
248
246
  get items() { return this.#items_accessor_storage; }
249
247
  set items(value) { this.#items_accessor_storage = value; }
250
248
  toString(long = false) {
251
249
  if (!long)
252
- return `<MetadataBlock @ ${this.offsetHex}>`;
250
+ return `<MetadataBlock @ ${hex(this.byteOffset)}>`;
253
251
  let text = [
254
- `---- Metadata block at ${this.offsetHex} ----`,
255
- `Checksum: 0x${this.checksum.toString(16).padStart(8, '0')}`,
252
+ `---- Metadata block at ${hex(this.byteOffset)} ----`,
253
+ `Checksum: ${hex(this.checksum)}`,
256
254
  `Last updated: ${new Date(Number(this.timestamp)).toLocaleString()}`,
257
- `Previous block: 0x${this.previous_offset.toString(16).padStart(8, '0')}`,
255
+ `Previous block: ${hex(this.previous_offset)}`,
258
256
  'Entries:',
259
257
  ].join('\n');
260
258
  for (const entry of this.items) {
261
259
  if (!entry.offset)
262
260
  continue;
263
- text += `\n\t0x${entry.id.toString(16).padStart(8, '0')}: ${format(entry.size).padStart(5)} at 0x${entry.offset.toString(16).padStart(8, '0')}`;
261
+ text += `\n\t${hex(entry.id)}: ${format(entry.size).padStart(5)} at ${hex(entry.offset)}`;
264
262
  }
265
263
  return text;
266
264
  }
@@ -276,28 +274,26 @@ let MetadataBlock = (() => {
276
274
  */
277
275
  waitUnlocked(depth = 0) {
278
276
  if (depth > max_lock_attempts)
279
- throw crit(withErrno('EBUSY', `sbfs: exceeded max attempts waiting for metadata block at ${this.offsetHex} to be unlocked`));
280
- const i = this.length - 1;
281
- if (!Atomics.load(this, i))
277
+ throw crit(withErrno('EBUSY', `sbfs: exceeded max attempts waiting for metadata block at ${hex(this.byteOffset)} to be unlocked`));
278
+ if (!Atomics.load(this, MetadataBlock.lockIndex))
282
279
  return;
283
- switch (Atomics.wait(this, i, 1)) {
280
+ switch (Atomics.wait(this, MetadataBlock.lockIndex, 1)) {
284
281
  case 'ok':
285
282
  break;
286
283
  case 'not-equal':
287
284
  depth++;
288
- err(`sbfs: waiting for metadata block at ${this.offsetHex} to be unlocked (${depth}/${max_lock_attempts})`);
285
+ err(`sbfs: waiting for metadata block at ${hex(this.byteOffset)} to be unlocked (${depth}/${max_lock_attempts})`);
289
286
  return this.waitUnlocked(depth);
290
287
  case 'timed-out':
291
- throw crit(withErrno('EBUSY', `sbfs: timed out waiting for metadata block at ${this.offsetHex} to be unlocked`));
288
+ throw crit(withErrno('EBUSY', `sbfs: timed out waiting for metadata block at ${hex(this.byteOffset)} to be unlocked`));
292
289
  }
293
290
  }
294
291
  lock() {
295
292
  this.waitUnlocked();
296
- const i = offsetof(this, 'locked');
297
- Atomics.store(this, i, 1);
293
+ Atomics.store(this, MetadataBlock.lockIndex, 1);
298
294
  const release = () => {
299
- Atomics.store(this, i, 0);
300
- Atomics.notify(this, i, 1);
295
+ Atomics.store(this, MetadataBlock.lockIndex, 0);
296
+ Atomics.notify(this, MetadataBlock.lockIndex, 1);
301
297
  };
302
298
  release[Symbol.dispose] = release;
303
299
  return release;
@@ -306,6 +302,9 @@ let MetadataBlock = (() => {
306
302
  super(...arguments);
307
303
  __runInitializers(this, _locked_extraInitializers);
308
304
  }
305
+ static {
306
+ __runInitializers(_classThis, _classExtraInitializers);
307
+ }
309
308
  };
310
309
  return MetadataBlock = _classThis;
311
310
  })();
@@ -425,7 +424,7 @@ let SuperBlock = (() => {
425
424
  throw crit(withErrno('EIO', 'sbfs: checksum mismatch for super block'));
426
425
  this.metadata = new MetadataBlock(this.buffer, this.metadata_offset);
427
426
  if (this.metadata.checksum !== checksum(this.metadata))
428
- throw crit(withErrno('EIO', `sbfs: checksum mismatch for metadata block (saved ${this.metadata.checksum.toString(16).padStart(8, '0')}, computed ${checksum(this.metadata).toString(16).padStart(8, '0')})`));
427
+ throw crit(withErrno('EIO', `sbfs: checksum mismatch for metadata block (saved ${hex(this.metadata.checksum)}, computed ${hex(checksum(this.metadata))})`));
429
428
  if (this.inode_format != _inode_version)
430
429
  throw crit(withErrno('EIO', 'sbfs: inode format mismatch'));
431
430
  if (this.metadata_block_size != sizeof(MetadataBlock))
@@ -506,6 +505,7 @@ let SuperBlock = (() => {
506
505
  this.metadata_offset = metadata.byteOffset;
507
506
  _update(metadata);
508
507
  _update(this);
508
+ debug(`sbfs: rotated metadata block at ${hex(metadata.previous_offset)} with new block at ${hex(offset)}`);
509
509
  return metadata;
510
510
  }
511
511
  /**
@@ -541,7 +541,10 @@ export { SuperBlock };
541
541
  * Note we don't include the checksum when computing a new one.
542
542
  */
543
543
  function checksum(value) {
544
- return crc32c(new Uint8Array(value.buffer, value.byteOffset + 4, sizeof(value) - 4));
544
+ let length = sizeof(value) - 4;
545
+ if (value instanceof MetadataBlock)
546
+ length -= Int32Array.BYTES_PER_ELEMENT;
547
+ return crc32c(new Uint8Array(value.buffer, value.byteOffset + 4, length));
545
548
  }
546
549
  /**
547
550
  * Update a block's checksum and timestamp.
package/dist/context.d.ts CHANGED
@@ -11,4 +11,4 @@ export declare const boundContexts: Map<number, BoundContext>;
11
11
  * Note that the default credentials of a bound context are copied from the global credentials.
12
12
  * @category Contexts
13
13
  */
14
- export declare function bindContext(this: void | null | FSContext, { root, pwd, credentials }?: ContextInit): BoundContext;
14
+ export declare function bindContext(this: V_Context, init?: ContextInit): BoundContext;
package/dist/context.js CHANGED
@@ -1,12 +1,10 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
+ import { UV } from 'kerium';
2
3
  import { bindFunctions } from 'utilium';
3
- import { defaultContext } from './internal/contexts.js';
4
- import { createCredentials } from './internal/credentials.js';
5
- import * as path from './path.js';
4
+ import { contextOf, createChildContext } from './internal/contexts.js';
6
5
  import * as fs from './node/index.js';
6
+ import * as path from './path.js';
7
7
  import * as xattr from './vfs/xattr.js';
8
- // 0 is reserved for the global/default context
9
- let _nextId = 1;
10
8
  /**
11
9
  * A map of all contexts.
12
10
  * @internal
@@ -18,19 +16,12 @@ export const boundContexts = new Map();
18
16
  * Note that the default credentials of a bound context are copied from the global credentials.
19
17
  * @category Contexts
20
18
  */
21
- export function bindContext({ root = this?.root || '/', pwd = this?.pwd || '/', credentials = structuredClone(defaultContext.credentials) } = {}) {
22
- const parent = this ?? defaultContext;
23
- const ctx = {
24
- id: _nextId++,
25
- root,
26
- pwd,
27
- credentials: createCredentials(credentials),
28
- descriptors: new Map(),
29
- parent,
30
- children: [],
31
- };
32
- const bound = {
33
- ...ctx,
19
+ export function bindContext(init = {}) {
20
+ const $ = contextOf(this);
21
+ if (!fs.statSync.call(this, $.root).isDirectory())
22
+ throw UV('ENOTDIR', { syscall: 'chroot', path: $.root });
23
+ const ctx = createChildContext($, init);
24
+ const bound = Object.assign(ctx, {
34
25
  fs: {
35
26
  ...bindFunctions(fs, ctx),
36
27
  promises: bindFunctions(fs.promises, ctx),
@@ -42,7 +33,7 @@ export function bindContext({ root = this?.root || '/', pwd = this?.pwd || '/',
42
33
  ctx.children.push(child);
43
34
  return child;
44
35
  },
45
- };
36
+ });
46
37
  boundContexts.set(ctx.id, bound);
47
38
  return bound;
48
39
  }
@@ -4,11 +4,18 @@ import type * as path from '../path.js';
4
4
  import type { Handle } from '../vfs/file.js';
5
5
  import type * as xattr from '../vfs/xattr.js';
6
6
  import type { Credentials, CredentialsInit } from './credentials.js';
7
+ /**
8
+ * Symbol used for context branding
9
+ * @internal @hidden
10
+ */
11
+ declare const kIsContext: unique symbol;
7
12
  /**
8
13
  * A context used for FS operations
9
14
  * @category Contexts
10
15
  */
11
16
  export interface FSContext {
17
+ /** The unique ID of the context */
18
+ readonly [kIsContext]: boolean;
12
19
  /** The unique ID of the context */
13
20
  readonly id: number;
14
21
  /**
@@ -22,16 +29,16 @@ export interface FSContext {
22
29
  /** The credentials of the context, used for access checks */
23
30
  readonly credentials: Credentials;
24
31
  /** A map of open file descriptors to their handles */
25
- descriptors: Map<number, Handle>;
32
+ readonly descriptors: Map<number, Handle>;
26
33
  /** The parent context, if any. */
27
- parent: V_Context;
34
+ readonly parent: FSContext | null;
28
35
  /** The child contexts */
29
- children: FSContext[];
36
+ readonly children: FSContext[];
30
37
  }
31
38
  /**
32
39
  * maybe an FS context
33
40
  */
34
- export type V_Context = void | null | (Partial<FSContext> & object);
41
+ export type V_Context = unknown;
35
42
  /**
36
43
  * Allows you to restrict operations to a specific root path and set of credentials.
37
44
  * @category Contexts
@@ -62,3 +69,16 @@ export interface ContextInit {
62
69
  * @category Contexts
63
70
  */
64
71
  export declare const defaultContext: FSContext;
72
+ export declare function contextOf($: unknown): FSContext;
73
+ /**
74
+ * Create a blank FS Context
75
+ * @internal
76
+ * @category Contexts
77
+ * @todo Make sure parent root can't be escaped
78
+ *
79
+ * This exists so that `kIsContext` is not exported and to make sure the context is "secure".
80
+ */
81
+ export declare function createChildContext(parent: FSContext, init?: ContextInit): FSContext & {
82
+ parent: FSContext;
83
+ };
84
+ export {};
@@ -1,10 +1,16 @@
1
1
  import { createCredentials } from './credentials.js';
2
+ /**
3
+ * Symbol used for context branding
4
+ * @internal @hidden
5
+ */
6
+ const kIsContext = Symbol('ZenFSContext');
2
7
  /**
3
8
  * The default/global context.
4
9
  * @internal @hidden
5
10
  * @category Contexts
6
11
  */
7
12
  export const defaultContext = {
13
+ [kIsContext]: true,
8
14
  id: 0,
9
15
  root: '/',
10
16
  pwd: '/',
@@ -13,3 +19,37 @@ export const defaultContext = {
13
19
  parent: null,
14
20
  children: [],
15
21
  };
22
+ export function contextOf($) {
23
+ return typeof $ === 'object' && $ !== null && kIsContext in $ ? $ : defaultContext;
24
+ }
25
+ // 0 is reserved for the global/default context
26
+ let _nextId = 1;
27
+ /**
28
+ * Create a blank FS Context
29
+ * @internal
30
+ * @category Contexts
31
+ * @todo Make sure parent root can't be escaped
32
+ *
33
+ * This exists so that `kIsContext` is not exported and to make sure the context is "secure".
34
+ */
35
+ export function createChildContext(parent, init = {}) {
36
+ const { root = parent.root, pwd = parent.pwd, credentials = structuredClone(parent.credentials) } = init;
37
+ const ctx = {
38
+ [kIsContext]: true,
39
+ id: _nextId++,
40
+ root,
41
+ pwd,
42
+ credentials: createCredentials(credentials),
43
+ descriptors: new Map(),
44
+ parent: parent,
45
+ children: [],
46
+ };
47
+ Object.defineProperties(ctx, {
48
+ id: { configurable: false, writable: false },
49
+ credentials: { configurable: false, writable: false },
50
+ descriptors: { configurable: false, writable: false },
51
+ parent: { configurable: false, writable: false },
52
+ children: { configurable: false, writable: false },
53
+ });
54
+ return ctx;
55
+ }
@@ -39,9 +39,9 @@ import { sizeof } from 'memium';
39
39
  import { $from, field, struct, types as t } from 'memium/decorators';
40
40
  import { decodeUTF8, encodeUTF8, pick } from 'utilium';
41
41
  import { BufferView } from 'utilium/buffer.js';
42
- import { Stats } from '../node/stats.js';
43
42
  import * as c from '../constants.js';
44
- import { defaultContext } from './contexts.js';
43
+ import { Stats } from '../node/stats.js';
44
+ import { contextOf } from './contexts.js';
45
45
  /**
46
46
  * Root inode
47
47
  * @hidden
@@ -620,7 +620,7 @@ export function isFIFO(metadata) {
620
620
  * @internal
621
621
  */
622
622
  export function hasAccess($, inode, access) {
623
- const credentials = $?.credentials || defaultContext.credentials;
623
+ const { credentials } = contextOf($);
624
624
  if (isSymbolicLink(inode) || credentials.euid === 0 || credentials.egid === 0)
625
625
  return true;
626
626
  let perm = 0;
package/dist/node/sync.js CHANGED
@@ -467,7 +467,7 @@ export function lutimesSync(path, atime, mtime) {
467
467
  lutimesSync;
468
468
  export function realpathSync(path, options) {
469
469
  const encoding = typeof options == 'string' ? options : (options?.encoding ?? 'utf8');
470
- path = normalizePath(path);
470
+ path = normalizePath(path, true);
471
471
  const { fullPath } = _sync.resolve(this, path);
472
472
  if (encoding == 'utf8' || encoding == 'utf-8')
473
473
  return fullPath;
package/dist/path.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
- import { defaultContext } from './internal/contexts.js';
2
+ import { contextOf } from './internal/contexts.js';
3
3
  import { globToRegex } from './utils.js';
4
4
  export const sep = '/';
5
5
  function validateObject(str, name) {
@@ -81,7 +81,7 @@ export function formatExt(ext) {
81
81
  }
82
82
  export function resolve(...parts) {
83
83
  let resolved = '';
84
- for (const part of [...parts.reverse(), this?.pwd ?? defaultContext.pwd]) {
84
+ for (const part of [...parts.reverse(), contextOf(this).pwd]) {
85
85
  if (!part?.length)
86
86
  continue;
87
87
  resolved = `${part}/${resolved}`;
@@ -334,7 +334,7 @@ export function format(pathObject) {
334
334
  return dir === pathObject.root ? `${dir}${base}` : `${dir}/${base}`;
335
335
  }
336
336
  export function parse(path) {
337
- const isAbsolute = path.startsWith('/');
337
+ const isAbsolute = path[0] === '/';
338
338
  const ret = { root: isAbsolute ? '/' : '', dir: '', base: '', ext: '', name: '' };
339
339
  if (path.length === 0)
340
340
  return ret;
package/dist/utils.d.ts CHANGED
@@ -2,6 +2,7 @@ import { type Exception } from 'kerium';
2
2
  import type * as fs from 'node:fs';
3
3
  import type { Worker as NodeWorker } from 'node:worker_threads';
4
4
  import { type OptionalTuple } from 'utilium';
5
+ import type { V_Context } from './internal/contexts.js';
5
6
  declare global {
6
7
  function atob(data: string): string;
7
8
  function btoa(data: string): string;
@@ -31,8 +32,9 @@ export declare function normalizeTime(time: fs.TimeLike): number;
31
32
  /**
32
33
  * Normalizes a path
33
34
  * @internal
35
+ * @todo clean this up and make it so `path.resolve` is only called when an explicit context is passed (i.e. `normalizePath(..., $)` to use `path.resolve`)
34
36
  */
35
- export declare function normalizePath(p: fs.PathLike, noResolve?: boolean): string;
37
+ export declare function normalizePath(this: V_Context, p: fs.PathLike, noResolve?: boolean): string;
36
38
  /**
37
39
  * Normalizes options
38
40
  * @param options options to normalize
package/dist/utils.js CHANGED
@@ -51,6 +51,7 @@ export function normalizeTime(time) {
51
51
  /**
52
52
  * Normalizes a path
53
53
  * @internal
54
+ * @todo clean this up and make it so `path.resolve` is only called when an explicit context is passed (i.e. `normalizePath(..., $)` to use `path.resolve`)
54
55
  */
55
56
  export function normalizePath(p, noResolve = false) {
56
57
  if (p instanceof URL) {
@@ -67,7 +68,7 @@ export function normalizePath(p, noResolve = false) {
67
68
  throw withErrno('EINVAL', 'Path can not be empty');
68
69
  p = p.replaceAll(/[/\\]+/g, '/');
69
70
  // Note: PWD is not resolved here, it is resolved later.
70
- return noResolve ? p : resolve(p);
71
+ return noResolve ? p : resolve.call(this, p);
71
72
  }
72
73
  /**
73
74
  * Normalizes options
package/dist/vfs/acl.js CHANGED
@@ -44,9 +44,9 @@ import { err } from 'kerium/log';
44
44
  import { packed, sizeof } from 'memium';
45
45
  import { $from, struct, types as t } from 'memium/decorators';
46
46
  import { BufferView } from 'utilium/buffer.js';
47
- import { defaultContext } from '../internal/contexts.js';
48
- import { Attributes } from '../internal/inode.js';
49
47
  import { R_OK, S_IRWXG, S_IRWXO, S_IRWXU, W_OK, X_OK } from '../constants.js';
48
+ import { contextOf } from '../internal/contexts.js';
49
+ import { Attributes } from '../internal/inode.js';
50
50
  import * as xattr from './xattr.js';
51
51
  const version = 2;
52
52
  export var Type;
@@ -206,7 +206,7 @@ export function check($, inode, access) {
206
206
  if (!shouldCheck)
207
207
  return true;
208
208
  inode.attributes ??= new Attributes();
209
- const { euid, egid } = $?.credentials ?? defaultContext.credentials;
209
+ const { euid, egid } = contextOf($).credentials;
210
210
  const data = inode.attributes.get('system.posix_acl_access');
211
211
  if (!data)
212
212
  return true;
package/dist/vfs/async.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { setUVMessage, UV } from 'kerium';
2
2
  import { decodeUTF8 } from 'utilium';
3
3
  import * as constants from '../constants.js';
4
- import { defaultContext } from '../internal/contexts.js';
4
+ import { contextOf } from '../internal/contexts.js';
5
5
  import { hasAccess, isDirectory, isSymbolicLink } from '../internal/inode.js';
6
6
  import { basename, dirname, join, parse, resolve as resolvePath } from '../path.js';
7
7
  import { normalizeMode, normalizePath } from '../utils.js';
@@ -17,6 +17,7 @@ import { emitChange } from './watchers.js';
17
17
  * @internal @hidden
18
18
  */
19
19
  export async function resolve($, path, preserveSymlinks, extra) {
20
+ path = resolvePath.call($, path);
20
21
  if (preserveSymlinks) {
21
22
  const resolved = resolveMount(path, $, extra);
22
23
  const stats = await resolved.fs.stat(resolved.path).catch(() => undefined);
@@ -74,7 +75,7 @@ export async function open($, path, opt) {
74
75
  throw UV('ENOTDIR', 'open', dirname(path));
75
76
  if (!opt.allowDirectory && mode & constants.S_IFDIR)
76
77
  throw UV('EISDIR', 'open', path);
77
- const { euid: uid, egid: gid } = $?.credentials ?? defaultContext.credentials;
78
+ const { euid: uid, egid: gid } = contextOf($).credentials;
78
79
  const inode = await fs.createFile(resolved, {
79
80
  mode,
80
81
  uid: parentStats.mode & constants.S_ISUID ? parentStats.uid : uid,
@@ -110,7 +111,7 @@ export async function readlink(path) {
110
111
  }
111
112
  export async function mkdir(path, options = {}) {
112
113
  path = normalizePath(path);
113
- const { euid: uid, egid: gid } = this?.credentials ?? defaultContext.credentials;
114
+ const { euid: uid, egid: gid } = contextOf(this).credentials;
114
115
  const { mode = 0o777, recursive } = options;
115
116
  const { fs, path: resolved } = resolveMount(path, this, { syscall: 'mkdir' });
116
117
  const __create = async (path, resolved, parent) => {
package/dist/vfs/file.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import { UV, withErrno } from 'kerium';
3
3
  import * as c from '../constants.js';
4
- import { defaultContext } from '../internal/contexts.js';
4
+ import { contextOf } from '../internal/contexts.js';
5
5
  import { _chown, InodeFlags, isBlockDevice, isCharacterDevice } from '../internal/inode.js';
6
6
  import '../polyfills.js';
7
7
  /**
@@ -371,7 +371,7 @@ export class Handle {
371
371
  * @internal @hidden
372
372
  */
373
373
  export function toFD(file) {
374
- const map = file.context?.descriptors ?? defaultContext.descriptors;
374
+ const map = contextOf(file.context).descriptors;
375
375
  const fd = Math.max(map.size ? Math.max(...map.keys()) + 1 : 0, 4);
376
376
  map.set(fd, file);
377
377
  return fd;
@@ -380,12 +380,12 @@ export function toFD(file) {
380
380
  * @internal @hidden
381
381
  */
382
382
  export function fromFD($, fd) {
383
- const map = $?.descriptors ?? defaultContext.descriptors;
383
+ const map = contextOf($).descriptors;
384
384
  const value = map.get(fd);
385
385
  if (!value)
386
386
  throw withErrno('EBADF');
387
387
  return value;
388
388
  }
389
389
  export function deleteFD($, fd) {
390
- return ($?.descriptors ?? defaultContext.descriptors).delete(fd);
390
+ return contextOf($).descriptors.delete(fd);
391
391
  }
@@ -4,7 +4,7 @@ import { Errno, Exception, UV, withErrno } from 'kerium';
4
4
  import { alert, debug, err, info, notice, warn } from 'kerium/log';
5
5
  import { InMemory } from '../backends/memory.js';
6
6
  import { size_max } from '../constants.js';
7
- import { defaultContext } from '../internal/contexts.js';
7
+ import { contextOf } from '../internal/contexts.js';
8
8
  import { credentialsAllowRoot } from '../internal/credentials.js';
9
9
  import { withExceptionContext } from '../internal/error.js';
10
10
  import { join, resolve } from '../path.js';
@@ -53,9 +53,10 @@ export function umount(mountPoint) {
53
53
  * @internal @hidden
54
54
  */
55
55
  export function resolveMount(path, ctx, extra) {
56
- const root = ctx?.root || defaultContext.root;
56
+ const { root } = contextOf(ctx);
57
57
  const _exceptionContext = { path, ...extra };
58
- path = normalizePath(join(root, path));
58
+ path = normalizePath(join(root, path), true);
59
+ path = resolve.call(ctx, path);
59
60
  const sortedMounts = [...mounts].sort((a, b) => (a[0].length > b[0].length ? -1 : 1)); // descending order of the string length
60
61
  for (const [mountPoint, fs] of sortedMounts) {
61
62
  // We know path is normalized, so it would be a substring of the mount point.
@@ -94,7 +95,7 @@ export function _statfs(fs, bigint) {
94
95
  * @category Backends and Configuration
95
96
  */
96
97
  export function chroot(path) {
97
- const $ = this ?? defaultContext;
98
+ const $ = contextOf(this);
98
99
  if (!credentialsAllowRoot($.credentials))
99
100
  throw withErrno('EPERM', 'Can not chroot() as non-root user');
100
101
  $.root ??= '/';
package/dist/vfs/sync.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { setUVMessage, UV } from 'kerium';
2
2
  import { decodeUTF8 } from 'utilium';
3
3
  import * as constants from '../constants.js';
4
- import { defaultContext } from '../internal/contexts.js';
4
+ import { contextOf } from '../internal/contexts.js';
5
5
  import { hasAccess, isDirectory, isSymbolicLink } from '../internal/inode.js';
6
6
  import { basename, dirname, join, parse, resolve as resolvePath } from '../path.js';
7
7
  import { normalizeMode, normalizePath } from '../utils.js';
@@ -18,6 +18,7 @@ import { emitChange } from './watchers.js';
18
18
  * @internal @hidden
19
19
  */
20
20
  export function resolve($, path, preserveSymlinks, extra) {
21
+ path = resolvePath.call($, path);
21
22
  /* Try to resolve it directly. If this works,
22
23
  that means we don't need to perform any resolution for parent directories. */
23
24
  try {
@@ -87,7 +88,7 @@ export function open(path, opt) {
87
88
  if (checkAccess && !hasAccess(this, parentStats, constants.W_OK)) {
88
89
  throw UV('EACCES', 'open', path);
89
90
  }
90
- const { euid: uid, egid: gid } = this?.credentials ?? defaultContext.credentials;
91
+ const { euid: uid, egid: gid } = contextOf(this).credentials;
91
92
  const inode = fs.createFileSync(resolved, {
92
93
  mode,
93
94
  uid: parentStats.mode & constants.S_ISUID ? parentStats.uid : uid,
@@ -124,7 +125,7 @@ export function readlink(path) {
124
125
  export function mkdir(path, options = {}) {
125
126
  path = normalizePath(path);
126
127
  const { fs, path: resolved } = resolve(this, path);
127
- const { euid: uid, egid: gid } = this?.credentials ?? defaultContext.credentials;
128
+ const { euid: uid, egid: gid } = contextOf(this).credentials;
128
129
  const { mode = 0o777, recursive } = options;
129
130
  const __create = (path, resolved, parent) => {
130
131
  if (checkAccess && !hasAccess(this, parent, constants.W_OK))
@@ -75,5 +75,5 @@ export declare function removeWatcher(path: string, watcher: FSWatcher): void;
75
75
  /**
76
76
  * @internal @hidden
77
77
  */
78
- export declare function emitChange($: V_Context, eventType: fs.WatchEventType, filename: string): void;
78
+ export declare function emitChange(context: V_Context, eventType: fs.WatchEventType, filename: string): void;
79
79
  export {};
@@ -1,9 +1,10 @@
1
1
  import { EventEmitter } from 'eventemitter3';
2
2
  import { UV } from 'kerium';
3
- import { basename, dirname, join, relative } from '../path.js';
4
- import { normalizePath } from '../utils.js';
3
+ import { contextOf } from '../internal/contexts.js';
5
4
  import { isStatsEqual } from '../node/stats.js';
6
5
  import { statSync } from '../node/sync.js';
6
+ import { basename, dirname, join, relative } from '../path.js';
7
+ import { normalizePath } from '../utils.js';
7
8
  /**
8
9
  * Base class for file system watchers.
9
10
  * Provides event handling capabilities for watching file system changes.
@@ -61,9 +62,10 @@ export class FSWatcher extends Watcher {
61
62
  options;
62
63
  realpath;
63
64
  constructor(context, path, options) {
64
- super(context, path);
65
+ const $ = contextOf(context);
66
+ super($, path);
65
67
  this.options = options;
66
- this.realpath = context?.root ? join(context.root, path) : path;
68
+ this.realpath = join($.root, path);
67
69
  addWatcher(this.realpath, this);
68
70
  }
69
71
  close() {
@@ -145,7 +147,8 @@ export function removeWatcher(path, watcher) {
145
147
  /**
146
148
  * @internal @hidden
147
149
  */
148
- export function emitChange($, eventType, filename) {
150
+ export function emitChange(context, eventType, filename) {
151
+ const $ = contextOf(context);
149
152
  if ($)
150
153
  filename = join($.root ?? '/', filename);
151
154
  filename = normalizePath(filename);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "2.4.2",
3
+ "version": "2.4.4",
4
4
  "description": "A filesystem, anywhere",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -1,8 +1,9 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import assert from 'node:assert';
3
+ import { randomBytes } from 'node:crypto';
3
4
  import { suite, test } from 'node:test';
4
5
  import { Worker } from 'worker_threads';
5
- import { fs, mount, resolveMountConfig, SingleBuffer } from '@zenfs/core';
6
+ import { fs, mount, resolveMountConfig, SingleBuffer, vfs } from '@zenfs/core';
6
7
  import { setupLogs } from '../logs.js';
7
8
 
8
9
  setupLogs();
@@ -51,4 +52,50 @@ await suite('SingleBuffer', () => {
51
52
 
52
53
  assert(fs.existsSync('/shared/worker-file.ts'));
53
54
  });
55
+
56
+ test('reliability across varied file sizes', async () => {
57
+ const mountPoint = '/sbfs-reliability';
58
+ const verifyMountPoint = '/sbfs-verify';
59
+ const buffer = new ArrayBuffer(0x400000);
60
+ const writable = await resolveMountConfig({ backend: SingleBuffer, buffer, label: 'reliability' });
61
+ mount(mountPoint, writable);
62
+
63
+ const filePath = `${mountPoint}/payload.bin`;
64
+ const growthSizes = [0, 1, 17, 512, 8192, 65535, 262144, 524288];
65
+ const shrinkSizes = [262144, 4096, 128, 0];
66
+
67
+ const verifySnapshot = async (expected: Buffer, size: number) => {
68
+ mount(verifyMountPoint, writable);
69
+ try {
70
+ const reopened = fs.readFileSync(`${verifyMountPoint}/payload.bin`);
71
+ assert.strictEqual(reopened.byteLength, size, `snapshot size mismatch for ${size} bytes`);
72
+ assert.deepStrictEqual(reopened, expected, `snapshot content mismatch for ${size} bytes`);
73
+ } finally {
74
+ vfs.umount(verifyMountPoint);
75
+ }
76
+ };
77
+
78
+ try {
79
+ for (const size of growthSizes) {
80
+ const payload = size ? randomBytes(size) : Buffer.alloc(0);
81
+ fs.writeFileSync(filePath, payload);
82
+ const direct = fs.readFileSync(filePath);
83
+ assert.strictEqual(direct.byteLength, size, `direct size mismatch for ${size} bytes`);
84
+ assert.deepStrictEqual(direct, payload, `direct content mismatch for ${size} bytes`);
85
+ await verifySnapshot(direct, size);
86
+ }
87
+
88
+ for (const size of shrinkSizes) {
89
+ const payload = size ? randomBytes(size) : Buffer.alloc(0);
90
+ fs.writeFileSync(filePath, payload);
91
+ const direct = fs.readFileSync(filePath);
92
+ assert.strictEqual(direct.byteLength, size, `direct size mismatch after shrink to ${size} bytes`);
93
+ assert.deepStrictEqual(direct, payload, `direct content mismatch after shrink to ${size} bytes`);
94
+ await verifySnapshot(direct, size);
95
+ }
96
+ } finally {
97
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
98
+ vfs.umount(mountPoint);
99
+ }
100
+ });
54
101
  });
@@ -60,8 +60,7 @@ suite('Context', () => {
60
60
  await promise;
61
61
  });
62
62
 
63
- test('Path resolution of / with context root and mount point being the same', async () => {
64
- // @zenfs/core#226
63
+ test('Path resolution of / with context root and mount point being the same #226', async () => {
65
64
  await configure({
66
65
  mounts: { '/bananas': InMemory },
67
66
  });
@@ -73,8 +72,7 @@ suite('Context', () => {
73
72
  assert.deepEqual(bananas.fs.readdirSync('/'), ['yellow']);
74
73
  });
75
74
 
76
- test('Different working directory', { todo: true }, () => {
77
- // @zenfs/core#263
75
+ test('Different working directory #263', () => {
78
76
  ctx.mkdirSync('/test');
79
77
  context.pwd = '/test';
80
78
 
@@ -19,8 +19,7 @@ suite('Links', () => {
19
19
  assert(stats.isSymbolicLink());
20
20
  });
21
21
 
22
- test('lstat file inside symlinked directory', async () => {
23
- // @zenfs/core#241
22
+ test('lstat file inside symlinked directory #241', async () => {
24
23
  await fs.promises.mkdir('/a');
25
24
  await fs.promises.writeFile('/a/hello.txt', 'hello world');
26
25
  await fs.promises.symlink('/a', '/b');
@@ -70,8 +70,7 @@ suite('read', () => {
70
70
  assert.equal(bytesRead, expected.length);
71
71
  });
72
72
 
73
- test('read using callback API', async () => {
74
- // @zenfs/core#239
73
+ test('read using callback API #239', async () => {
75
74
  const path = '/text.txt';
76
75
 
77
76
  fs.writeFileSync(path, 'hello world');