@zenfs/core 2.6.5 → 2.7.0

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/vfs/sync.js CHANGED
@@ -113,11 +113,11 @@ export function resolve($, path, preserveSymlinks, extra) {
113
113
  * @category VFS
114
114
  * @internal
115
115
  */
116
- export function open(path, opt) {
116
+ export function open($, path, opt) {
117
117
  path = normalizePath(path);
118
118
  const mode = normalizeMode(opt.mode, 0o644), flag = flags.parse(opt.flag);
119
- path = opt.preserveSymlinks ? path : resolve(this, path).fullPath;
120
- const { fs, path: resolved } = resolveMount(path, this);
119
+ path = opt.preserveSymlinks ? path : resolve($, path).fullPath;
120
+ const { fs, path: resolved } = resolveMount(path, $);
121
121
  let stats;
122
122
  try {
123
123
  stats = fs.statSync(resolved);
@@ -134,25 +134,25 @@ export function open(path, opt) {
134
134
  // Create the file
135
135
  const parentPath = dirname(resolved);
136
136
  const parentStats = fs.statSync(parentPath);
137
- if (checkAccess && !hasAccess(this, parentStats, constants.W_OK)) {
137
+ if (checkAccess && !hasAccess($, parentStats, constants.W_OK)) {
138
138
  throw UV('EACCES', 'open', path);
139
139
  }
140
140
  if (!isDirectory(parentStats)) {
141
141
  throw UV('ENOTDIR', 'open', path);
142
142
  }
143
- if (!opt.allowDirectory && mode & constants.S_IFDIR)
143
+ if (!opt.allowDirectory && isDirectory({ mode }))
144
144
  throw UV('EISDIR', 'open', path);
145
145
  // Serialize entry creation with other operations on the parent directory
146
146
  const _ = __addDisposableResource(env_1, lockPathSync(fs, parentPath, 'rw', parentStats), false);
147
- const { euid: uid, egid: gid } = contextOf(this).credentials;
147
+ const { euid: uid, egid: gid } = contextOf($).credentials;
148
148
  const inode = fs.createFileSync(resolved, {
149
149
  mode,
150
150
  uid: parentStats.mode & constants.S_ISUID ? parentStats.uid : uid,
151
151
  gid: parentStats.mode & constants.S_ISGID ? parentStats.gid : gid,
152
152
  });
153
153
  // A new entry in the parent directory, which is a 'rename' event
154
- emitChange(this, 'rename', path);
155
- return new Handle(this, path, resolved, flag, cacheOf(fs).ref(resolved, inode));
154
+ emitChange($, 'rename', path);
155
+ return new Handle($, path, resolved, flag, cacheOf(fs).ref(resolved, inode));
156
156
  }
157
157
  catch (e_1) {
158
158
  env_1.error = e_1;
@@ -162,14 +162,14 @@ export function open(path, opt) {
162
162
  __disposeResources(env_1);
163
163
  }
164
164
  }
165
- if (checkAccess && (!hasAccess(this, stats, mode) || !hasAccess(this, stats, flags.toMode(flag)))) {
165
+ if (checkAccess && (!hasAccess($, stats, mode) || !hasAccess($, stats, flags.toMode(flag)))) {
166
166
  throw UV('EACCES', 'open', path);
167
167
  }
168
168
  if (flag & constants.O_EXCL)
169
169
  throw UV('EEXIST', 'open', path);
170
- if (!opt.allowDirectory && stats.mode & constants.S_IFDIR)
170
+ if (!opt.allowDirectory && isDirectory(stats))
171
171
  throw UV('EISDIR', 'open', path);
172
- const file = new Handle(this, path, resolved, flag, cacheOf(fs).ref(resolved, stats));
172
+ const file = new Handle($, path, resolved, flag, cacheOf(fs).ref(resolved, stats));
173
173
  if (flag & constants.O_TRUNC)
174
174
  file.truncateSync(0);
175
175
  return file;
@@ -50,6 +50,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
50
50
  var e = new Error(message);
51
51
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
52
  });
53
+ import { err } from 'kerium/log';
53
54
  import { VNode } from './vnode.js';
54
55
  /**
55
56
  * The VFS-level cache of vnodes for a single file system. Modeled after Linux's dcache/icache.
@@ -76,6 +77,8 @@ export class VCache {
76
77
  */
77
78
  ref(path, inode) {
78
79
  let node = this.byIno.get(inode.ino);
80
+ if (node && node.inode.nlink === node.paths.size && !node.paths.has(path))
81
+ err(`vcache.ref: vnode for ${this.fs.label || this.fs.uuid}:${inode.ino} has an nlink of ${node.inode.nlink} but referenced by more paths [#314]`);
79
82
  if (!node) {
80
83
  node = new VNode(this.fs, path, inode);
81
84
  this.byIno.set(inode.ino, node);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "2.6.5",
3
+ "version": "2.7.0",
4
4
  "description": "A filesystem, anywhere",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -93,5 +93,14 @@
93
93
  "typedoc": "^0.28.18",
94
94
  "typescript": "^6.0.0",
95
95
  "typescript-eslint": "^8.58.0"
96
+ },
97
+ "prettier": {
98
+ "singleQuote": true,
99
+ "useTabs": true,
100
+ "trailingComma": "es5",
101
+ "tabWidth": 4,
102
+ "printWidth": 150,
103
+ "arrowParens": "avoid",
104
+ "experimentalOperatorPosition": "start"
96
105
  }
97
106
  }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { lstatSync, readdirSync, writeFileSync, constants as c } from 'node:fs';
2
+ import { constants as c, lstatSync, readdirSync, writeFileSync } from 'node:fs';
3
3
  import { join, matchesGlob, relative, resolve } from 'node:path/posix';
4
4
  import { parseArgs, styleText } from 'node:util';
5
5
 
@@ -9,6 +9,7 @@ const { values: options, positionals } = parseArgs({
9
9
  ignore: { short: 'i', type: 'string', multiple: true, default: [] },
10
10
  output: { short: 'o', type: 'string', default: 'index.json' },
11
11
  quiet: { short: 'q', type: 'boolean', default: false },
12
+ xdev: { type: 'boolean', default: false },
12
13
  verbose: { type: 'boolean', default: false },
13
14
  },
14
15
  allowPositionals: true,
@@ -22,11 +23,12 @@ if (options.help) {
22
23
  path: The path to create a listing for
23
24
 
24
25
  options:
25
- --help, -h Outputs this help message
26
- --quiet, -q The command will not generate any output, including error messages.
27
- --verbose Output verbose messages
28
- --output, -o <path> Path to the output file. Defaults to listing.
29
- --ignore, -i <pattern> Ignores files which match the glob <pattern>. Can be passed multiple times.
26
+ -h, --help Outputs this help message
27
+ -q, --quiet The command will not generate any output, including error messages.
28
+ --verbose Output verbose messages
29
+ --xdev Force crossing device/FS boundaries
30
+ -o, --output <path> Path to the output file. Defaults to listing.
31
+ -i, --ignore <pattern> Ignores files which match the glob <pattern>. Can be passed multiple times.
30
32
  `);
31
33
  process.exit();
32
34
  }
@@ -75,6 +77,8 @@ function getTypeText(/** @type {{ mode: number }} */ stats) {
75
77
  }
76
78
  }
77
79
 
80
+ const seenDevs = new Set();
81
+
78
82
  /**
79
83
  * @param {string} path
80
84
  */
@@ -86,6 +90,17 @@ function computeEntries(path) {
86
90
  }
87
91
 
88
92
  const stats = lstatSync(path);
93
+ if (!seenDevs.size) seenDevs.add(stats.dev);
94
+ else if (!seenDevs.has(stats.dev))
95
+ if (options.xdev) {
96
+ console.warn(
97
+ `${styleText('yellowBright', `--xdev: Adding entries from device ${stats.dev} (${path}). You may get duplicate inos which MUST be de-duplicated manually.`)}`
98
+ );
99
+ seenDevs.add(stats.dev);
100
+ } else {
101
+ console.warn(`${styleText('yellowBright', `Ignoring ${path} because it crosses a device boundary`)}`);
102
+ return;
103
+ }
89
104
 
90
105
  const type = getTypeText(stats);
91
106
 
@@ -97,7 +112,7 @@ function computeEntries(path) {
97
112
  }
98
113
  } catch (/** @type {any} */ e) {
99
114
  if (!options.quiet) {
100
- console.log(`${styleText('red', 'fail')} ${path}: ${e.message}`);
115
+ console.log(`${styleText('red', 'fail')} ${path}: ${e instanceof Error ? e.message : String(e)}`);
101
116
  }
102
117
  }
103
118
  }
@@ -0,0 +1,33 @@
1
+ // SPDX-License-Identifier: LGPL-3.0-or-later
2
+ import { CopyOnWriteFS, InMemoryStore, StoreFS } from '@zenfs/core';
3
+ import assert from 'node:assert/strict';
4
+ import { suite, test } from 'node:test';
5
+ import { setupLogs } from '../logs.js';
6
+ setupLogs();
7
+
8
+ const size = 1 << 20;
9
+
10
+ const options = { mode: 0o644, uid: 0, gid: 0 };
11
+
12
+ suite('CopyOnWrite', () => {
13
+ test('Truncating a base-layer file does not copy up the bytes it discards #313', async () => {
14
+ const readable = new StoreFS(new InMemoryStore(undefined, 'ro'));
15
+ await readable.ready();
16
+ await readable.createFile('/big', options);
17
+ await readable.write('/big', new Uint8Array(size).fill(0x41), 0);
18
+
19
+ const writable = new StoreFS(new InMemoryStore(undefined, 'rw'));
20
+ await writable.ready();
21
+
22
+ const fs = new CopyOnWriteFS(readable, writable);
23
+ await fs.touch('/big', { ...(await fs.stat('/big')), size: 4 });
24
+
25
+ assert.equal((await fs.stat('/big')).size, 4);
26
+
27
+ await using tx = writable.transaction();
28
+ const data = (await tx.get((await writable.stat('/big')).data))!;
29
+
30
+ assert.equal(data.byteLength, 4);
31
+ assert.equal(data.buffer.byteLength, 4);
32
+ });
33
+ });
@@ -68,6 +68,14 @@ suite('CopyOnWrite over uncached Fetch #301', () => {
68
68
  // Writes go through the vnode cache, so this surfaces when the handle is flushed rather than at `write`.
69
69
  assert.throws(() => fs.fsyncSync(fd), { code: 'EAGAIN' });
70
70
  });
71
+
72
+ test('Contents follow the size across the layers #313', async () => {
73
+ await fs.promises.truncate('/cow/a1.js', 4);
74
+ assert.equal(await fs.promises.readFile('/cow/a1.js', 'utf8'), '// C');
75
+
76
+ await fs.promises.truncate('/cow/a1.js', 10);
77
+ assert.equal(await fs.promises.readFile('/cow/a1.js', 'utf8'), '// C\0\0\0\0\0\0');
78
+ });
71
79
  });
72
80
 
73
81
  after(async () => {
@@ -34,6 +34,17 @@ suite('Truncating', config('truncate'), () => {
34
34
  fs.closeSync(fd);
35
35
  });
36
36
 
37
+ test('Contents follow the size #313', config('sync'), () => {
38
+ const path = 'truncate-contents.txt';
39
+ fs.writeFileSync(path, 'SECRETDATA');
40
+
41
+ fs.truncateSync(path, 4);
42
+ assert.equal(fs.readFileSync(path, 'utf8'), 'SECR');
43
+
44
+ fs.truncateSync(path, 10);
45
+ assert.equal(fs.readFileSync(path, 'utf8'), 'SECR\0\0\0\0\0\0');
46
+ });
47
+
37
48
  const statSize = async (path: string) => (await fs.promises.stat(path)).size;
38
49
 
39
50
  test('Async path functions', config('async'), async () => {
@@ -64,4 +75,15 @@ suite('Truncating', config('truncate'), () => {
64
75
 
65
76
  await handle.close();
66
77
  });
78
+
79
+ test('Contents follow the size (async) #313', config('async'), async () => {
80
+ const path = 'truncate-contents-async.txt';
81
+ await fs.promises.writeFile(path, 'SECRETDATA');
82
+
83
+ await fs.promises.truncate(path, 4);
84
+ assert.equal(await fs.promises.readFile(path, 'utf8'), 'SECR');
85
+
86
+ await fs.promises.truncate(path, 10);
87
+ assert.equal(await fs.promises.readFile(path, 'utf8'), 'SECR\0\0\0\0\0\0');
88
+ });
67
89
  });