@zenfs/core 2.6.1 → 2.6.3
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/backends/cow.d.ts +1 -0
- package/dist/backends/cow.js +49 -10
- package/dist/backends/single_buffer.js +34 -11
- package/dist/context.js +1 -0
- package/dist/internal/contexts.d.ts +3 -1
- package/dist/internal/contexts.js +11 -1
- package/dist/internal/inode.d.ts +2 -0
- package/dist/internal/inode.js +2 -0
- package/dist/internal/rpc.d.ts +4 -4
- package/dist/node/async.d.ts +11 -1
- package/dist/node/async.js +14 -35
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.js +1 -0
- package/dist/node/promises.d.ts +17 -3
- package/dist/node/promises.js +41 -17
- package/dist/node/stats.d.ts +12 -0
- package/dist/node/stats.js +35 -6
- package/dist/node/sync.d.ts +39 -11
- package/dist/node/sync.js +52 -22
- package/dist/node/utf8stream.d.ts +99 -0
- package/dist/node/utf8stream.js +690 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +17 -0
- package/dist/vfs/async.js +29 -21
- package/dist/vfs/shared.js +9 -7
- package/dist/vfs/sync.js +35 -21
- package/package.json +4 -4
- package/tests/backend/single-buffer-rotate.worker.js +19 -0
- package/tests/backend/single-buffer.test.ts +106 -0
- package/tests/common/context.test.ts +46 -0
- package/tests/common/glob.test.ts +85 -0
- package/tests/common/utf8stream.test.ts +164 -0
- package/tests/fs/concurrency.test.ts +23 -0
- package/tests/fs/directory.test.ts +21 -0
- package/tests/fs/links.test.ts +146 -0
- package/tests/fs/stat.test.ts +30 -1
- package/tests/native/setup.ts +1 -0
package/dist/utils.js
CHANGED
|
@@ -51,6 +51,14 @@ export function validateFD(fd) {
|
|
|
51
51
|
code: 'ERR_OUT_OF_RANGE',
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether an error means the path does not resolve to an entry. Used for `throwIfNoEntry`.
|
|
56
|
+
* @internal @hidden
|
|
57
|
+
*/
|
|
58
|
+
export function _isNoEntry(e) {
|
|
59
|
+
const { code } = (e ?? {});
|
|
60
|
+
return code == 'ENOENT' || code == 'ENOTDIR';
|
|
61
|
+
}
|
|
54
62
|
/**
|
|
55
63
|
* Normalizes a time to milliseconds since the epoch.
|
|
56
64
|
* Note numbers and numeric strings are seconds, like Node's `toUnixTimestamp`.
|
|
@@ -140,3 +148,12 @@ export function _tempDirName(prefix) {
|
|
|
140
148
|
// Like Node, the prefix is a path. A relative one is resolved against the working directory, not `/tmp`.
|
|
141
149
|
return `${normalizePath(prefix, true)}${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
142
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Resolves the global `Temporal`, which is not implemented by all runtimes.
|
|
153
|
+
* @internal @hidden
|
|
154
|
+
*/
|
|
155
|
+
export function _temporal() {
|
|
156
|
+
if ('Temporal' in globalThis && typeof globalThis.Temporal !== 'undefined')
|
|
157
|
+
return globalThis.Temporal;
|
|
158
|
+
throw withErrno('ENOTSUP', 'Temporal is not supported by this runtime');
|
|
159
|
+
}
|
package/dist/vfs/async.js
CHANGED
|
@@ -102,7 +102,7 @@ export async function resolve($, path, preserveSymlinks, extra) {
|
|
|
102
102
|
throw setUVMessage(Object.assign(e, { syscall: 'stat', path: maybePath, ...extra }));
|
|
103
103
|
}));
|
|
104
104
|
if (!stats)
|
|
105
|
-
return { ...resolved, fullPath:
|
|
105
|
+
return { ...resolved, fullPath: maybePath };
|
|
106
106
|
if (!isSymbolicLink(stats)) {
|
|
107
107
|
return { ...resolved, fullPath: maybePath, stats };
|
|
108
108
|
}
|
|
@@ -184,16 +184,36 @@ export async function readlink(path) {
|
|
|
184
184
|
return decodeUTF8(data);
|
|
185
185
|
}
|
|
186
186
|
export async function mkdir(path, options = {}) {
|
|
187
|
-
|
|
187
|
+
const original = normalizePath(path);
|
|
188
188
|
const { euid: uid, egid: gid } = contextOf(this).credentials;
|
|
189
189
|
const { mode = 0o777, recursive } = options;
|
|
190
|
-
const
|
|
191
|
-
const
|
|
190
|
+
const $ex = { syscall: 'mkdir', path: original };
|
|
191
|
+
const { fullPath: realParent } = await resolve(this, dirname(original), false, $ex);
|
|
192
|
+
const { fs, path: target } = resolveMount(join(realParent, basename(original)), this, $ex);
|
|
193
|
+
const followed = realParent != dirname(original);
|
|
194
|
+
let firstCreated;
|
|
195
|
+
const __create = async (path, resolved) => {
|
|
192
196
|
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
193
197
|
try {
|
|
198
|
+
const parentPath = dirname(resolved);
|
|
199
|
+
const parent = recursive && parentPath != '/' ? await __create(dirname(path), parentPath) : await fs.stat(parentPath);
|
|
200
|
+
const _ = __addDisposableResource(env_2, await lockPath(fs, parentPath, 'rw', parent), false);
|
|
201
|
+
if (recursive) {
|
|
202
|
+
const existing = cacheOf(fs).get(resolved)?.inode ?? (await fs.stat(resolved).catch(() => null));
|
|
203
|
+
if (existing) {
|
|
204
|
+
const stats = isSymbolicLink(existing) ? (await resolve(this, path, false, $ex)).stats : existing;
|
|
205
|
+
if (!stats)
|
|
206
|
+
throw UV('ENOENT', $ex);
|
|
207
|
+
if (!isDirectory(stats))
|
|
208
|
+
throw UV(resolved == target ? 'EEXIST' : 'ENOTDIR', $ex);
|
|
209
|
+
return existing;
|
|
210
|
+
}
|
|
211
|
+
// Node reports the link itself with ENOTDIR here, while `mkdirSync` reports the requested path with ENOENT
|
|
212
|
+
if (followed && (await fs.exists(join(parentPath, basename(path)))))
|
|
213
|
+
throw UV('ENOTDIR', 'mkdir', path);
|
|
214
|
+
}
|
|
194
215
|
if (checkAccess && !hasAccess(this, parent, constants.W_OK))
|
|
195
216
|
throw UV('EACCES', 'mkdir', path);
|
|
196
|
-
const _ = __addDisposableResource(env_2, await lockPath(fs, dirname(resolved), 'rw', parent), false);
|
|
197
217
|
const inode = await fs
|
|
198
218
|
.mkdir(resolved, {
|
|
199
219
|
mode,
|
|
@@ -201,6 +221,8 @@ export async function mkdir(path, options = {}) {
|
|
|
201
221
|
gid: parent.mode & constants.S_ISGID ? parent.gid : gid,
|
|
202
222
|
})
|
|
203
223
|
.catch(rethrow({ syscall: 'mkdir', path }));
|
|
224
|
+
if (recursive)
|
|
225
|
+
firstCreated ??= path;
|
|
204
226
|
emitChange(this, 'rename', path);
|
|
205
227
|
return inode;
|
|
206
228
|
}
|
|
@@ -212,22 +234,8 @@ export async function mkdir(path, options = {}) {
|
|
|
212
234
|
__disposeResources(env_2);
|
|
213
235
|
}
|
|
214
236
|
};
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
const dirs = [];
|
|
220
|
-
let origDir = path;
|
|
221
|
-
for (let dir = resolved; !(await fs.exists(dir)); dir = dirname(dir), origDir = dirname(origDir)) {
|
|
222
|
-
dirs.unshift([origDir, dir]);
|
|
223
|
-
}
|
|
224
|
-
if (!dirs.length)
|
|
225
|
-
return;
|
|
226
|
-
const stats = [await fs.stat(dirname(dirs[0][1]))];
|
|
227
|
-
for (const [i, [path, resolved]] of dirs.entries()) {
|
|
228
|
-
stats.push(await __create(path, resolved, stats[i]));
|
|
229
|
-
}
|
|
230
|
-
return dirs[0][0];
|
|
237
|
+
await __create(original, target);
|
|
238
|
+
return firstCreated;
|
|
231
239
|
}
|
|
232
240
|
export async function readdir(path, options = {}) {
|
|
233
241
|
path = normalizePath(path);
|
package/dist/vfs/shared.js
CHANGED
|
@@ -97,14 +97,16 @@ export function resolveMount(path, ctx, extra) {
|
|
|
97
97
|
export function _statfs(fs, bigint) {
|
|
98
98
|
const md = fs.usage();
|
|
99
99
|
const bs = md.blockSize || 4096;
|
|
100
|
+
const type = bigint ? BigInt : Number;
|
|
100
101
|
return {
|
|
101
|
-
type: (
|
|
102
|
-
bsize: (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
type: type(fs.type),
|
|
103
|
+
bsize: type(bs),
|
|
104
|
+
frsize: type(bs),
|
|
105
|
+
ffree: type(md.freeNodes || size_max),
|
|
106
|
+
files: type(md.totalNodes || size_max),
|
|
107
|
+
bavail: type(md.freeSpace / bs),
|
|
108
|
+
bfree: type(md.freeSpace / bs),
|
|
109
|
+
blocks: type(md.totalSpace / bs),
|
|
108
110
|
};
|
|
109
111
|
}
|
|
110
112
|
/**
|
package/dist/vfs/sync.js
CHANGED
|
@@ -100,7 +100,7 @@ export function resolve($, path, preserveSymlinks, extra) {
|
|
|
100
100
|
}
|
|
101
101
|
catch (e) {
|
|
102
102
|
if (e.code === 'ENOENT')
|
|
103
|
-
return { ...resolved, fullPath:
|
|
103
|
+
return { ...resolved, fullPath: maybePath };
|
|
104
104
|
throw setUVMessage(Object.assign(e, { syscall: 'stat', path: maybePath, ...extra }));
|
|
105
105
|
}
|
|
106
106
|
if (!isSymbolicLink(stats)) {
|
|
@@ -189,21 +189,48 @@ export function readlink(path) {
|
|
|
189
189
|
return decodeUTF8(data);
|
|
190
190
|
}
|
|
191
191
|
export function mkdir(path, options = {}) {
|
|
192
|
-
|
|
193
|
-
const {
|
|
192
|
+
const original = normalizePath(path);
|
|
193
|
+
const $ex = { syscall: 'mkdir', path: original };
|
|
194
|
+
const { fullPath: realParent } = resolve(this, dirname(original), false, $ex);
|
|
195
|
+
const { fs, path: target } = resolveMount(join(realParent, basename(original)), this, $ex);
|
|
196
|
+
const followed = realParent != dirname(original);
|
|
194
197
|
const { euid: uid, egid: gid } = contextOf(this).credentials;
|
|
195
198
|
const { mode = 0o777, recursive } = options;
|
|
196
|
-
|
|
199
|
+
let firstCreated;
|
|
200
|
+
const __create = (path, resolved) => {
|
|
197
201
|
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
198
202
|
try {
|
|
203
|
+
const parentPath = dirname(resolved);
|
|
204
|
+
const parent = recursive && parentPath != '/' ? __create(dirname(path), parentPath) : fs.statSync(parentPath);
|
|
205
|
+
if (recursive) {
|
|
206
|
+
let existing;
|
|
207
|
+
try {
|
|
208
|
+
existing = cacheOf(fs).get(resolved)?.inode ?? fs.statSync(resolved);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// It doesn't exist yet
|
|
212
|
+
}
|
|
213
|
+
if (existing) {
|
|
214
|
+
const stats = isSymbolicLink(existing) ? resolve(this, path, false, $ex).stats : existing;
|
|
215
|
+
if (!stats)
|
|
216
|
+
throw UV('ENOENT', $ex);
|
|
217
|
+
if (!isDirectory(stats))
|
|
218
|
+
throw UV(resolved == target ? 'EEXIST' : 'ENOTDIR', $ex);
|
|
219
|
+
return existing;
|
|
220
|
+
}
|
|
221
|
+
if (followed && fs.existsSync(join(parentPath, basename(path))))
|
|
222
|
+
throw UV('ENOENT', $ex);
|
|
223
|
+
}
|
|
199
224
|
if (checkAccess && !hasAccess(this, parent, constants.W_OK))
|
|
200
|
-
throw UV('EACCES', 'mkdir',
|
|
201
|
-
const _ = __addDisposableResource(env_2, lockPathSync(fs,
|
|
225
|
+
throw UV('EACCES', 'mkdir', path);
|
|
226
|
+
const _ = __addDisposableResource(env_2, lockPathSync(fs, parentPath, 'rw', parent), false);
|
|
202
227
|
const inode = wrap(fs, 'mkdirSync', { path, syscall: 'mkdir' })(resolved, {
|
|
203
228
|
mode,
|
|
204
229
|
uid: parent.mode & constants.S_ISUID ? parent.uid : uid,
|
|
205
230
|
gid: parent.mode & constants.S_ISGID ? parent.gid : gid,
|
|
206
231
|
});
|
|
232
|
+
if (recursive)
|
|
233
|
+
firstCreated ??= path;
|
|
207
234
|
emitChange(this, 'rename', path);
|
|
208
235
|
return inode;
|
|
209
236
|
}
|
|
@@ -215,21 +242,8 @@ export function mkdir(path, options = {}) {
|
|
|
215
242
|
__disposeResources(env_2);
|
|
216
243
|
}
|
|
217
244
|
};
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
const dirs = [];
|
|
223
|
-
for (let dir = resolved, original = path; !fs.existsSync(dir); dir = dirname(dir), original = dirname(original)) {
|
|
224
|
-
dirs.unshift({ resolved: dir, original });
|
|
225
|
-
}
|
|
226
|
-
if (!dirs.length)
|
|
227
|
-
return;
|
|
228
|
-
const stats = [fs.statSync(dirname(dirs[0].resolved))];
|
|
229
|
-
for (const [i, dir] of dirs.entries()) {
|
|
230
|
-
stats.push(__create(dir.original, dir.resolved, stats[i]));
|
|
231
|
-
}
|
|
232
|
-
return dirs[0].original;
|
|
245
|
+
__create(original, target);
|
|
246
|
+
return firstCreated;
|
|
233
247
|
}
|
|
234
248
|
export function readdir(path, options = {}) {
|
|
235
249
|
path = normalizePath(path);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenfs/core",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.3",
|
|
4
4
|
"description": "A filesystem, anywhere",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -71,18 +71,18 @@
|
|
|
71
71
|
"prepublishOnly": "npm run build"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"@types/node": "^
|
|
74
|
+
"@types/node": "^26.0.0",
|
|
75
75
|
"buffer": "^6.0.3",
|
|
76
76
|
"eventemitter3": "^5.0.1",
|
|
77
77
|
"kerium": "^1.4.2",
|
|
78
|
-
"memium": "^0.
|
|
78
|
+
"memium": "^1.0.0",
|
|
79
79
|
"readable-stream": "^4.5.2",
|
|
80
80
|
"utilium": "^3.0.0"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@eslint/js": "^10.0.1",
|
|
84
84
|
"@octokit/action": "^8.0.4",
|
|
85
|
-
"c8": "^
|
|
85
|
+
"c8": "^12.0.0",
|
|
86
86
|
"eslint": "^10.1.0",
|
|
87
87
|
"globals": "^17.3.0",
|
|
88
88
|
"prettier": "^3.2.5",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
3
|
+
import { SuperBlock } from '../../dist/backends/single_buffer.js';
|
|
4
|
+
import { setupLogs } from '../logs.js';
|
|
5
|
+
|
|
6
|
+
setupLogs('<worker>');
|
|
7
|
+
|
|
8
|
+
const { buffer, gate, rotations } = workerData;
|
|
9
|
+
|
|
10
|
+
const superblock = new SuperBlock(buffer);
|
|
11
|
+
|
|
12
|
+
parentPort.postMessage('ready');
|
|
13
|
+
|
|
14
|
+
Atomics.wait(gate, 0, 0, 1000);
|
|
15
|
+
|
|
16
|
+
const offsets = [];
|
|
17
|
+
for (let i = 0; i < rotations; i++) offsets.push(superblock.rotateMetadata().byteOffset);
|
|
18
|
+
|
|
19
|
+
parentPort.postMessage(offsets);
|
|
@@ -3,7 +3,9 @@ import assert from 'node:assert';
|
|
|
3
3
|
import { randomBytes } from 'node:crypto';
|
|
4
4
|
import { suite, test } from 'node:test';
|
|
5
5
|
import { Worker } from 'worker_threads';
|
|
6
|
+
import { sizeof } from 'memium';
|
|
6
7
|
import { fs, mount, resolveMountConfig, SingleBuffer, vfs } from '@zenfs/core';
|
|
8
|
+
import { MetadataBlock, SuperBlock } from '@zenfs/core/backends/single_buffer.js';
|
|
7
9
|
import { setupLogs } from '../logs.js';
|
|
8
10
|
|
|
9
11
|
setupLogs();
|
|
@@ -53,6 +55,88 @@ await suite('SingleBuffer', () => {
|
|
|
53
55
|
assert(fs.existsSync('/shared/worker-file.ts'));
|
|
54
56
|
});
|
|
55
57
|
|
|
58
|
+
test('aligns metadata when used_bytes is unaligned #309', () => {
|
|
59
|
+
// Writers reserve data of any length, so used_bytes can sit at any remainder when the metadata block is rotated.
|
|
60
|
+
for (const used of [8192n, 8193n, 8194n, 8195n]) {
|
|
61
|
+
const superblock = new SuperBlock(new ArrayBuffer(0x100000));
|
|
62
|
+
superblock.used_bytes = used;
|
|
63
|
+
|
|
64
|
+
const metadata = superblock.rotateMetadata();
|
|
65
|
+
const expected = Number((used + 3n) & ~3n);
|
|
66
|
+
|
|
67
|
+
assert.strictEqual(metadata.byteOffset, expected, `metadata offset for used_bytes ${used}`);
|
|
68
|
+
assert.strictEqual(superblock.used_bytes, BigInt(expected + sizeof(MetadataBlock)), `used_bytes after rotating from ${used}`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('refuses to rotate metadata past the end of the filesystem', () => {
|
|
73
|
+
const exhausted = new SuperBlock(new ArrayBuffer(sizeof(SuperBlock) + sizeof(MetadataBlock) + 4));
|
|
74
|
+
const exhaustedUsed = exhausted.used_bytes;
|
|
75
|
+
|
|
76
|
+
assert.throws(() => exhausted.rotateMetadata(), { code: 'ENOSPC' }, 'rotating without room should fail with ENOSPC');
|
|
77
|
+
assert.strictEqual(exhausted.used_bytes, exhaustedUsed, 'a failed rotation must not consume space');
|
|
78
|
+
|
|
79
|
+
const constrained = new SuperBlock(new ArrayBuffer(0x100000));
|
|
80
|
+
constrained.total_bytes = BigInt(sizeof(SuperBlock) + sizeof(MetadataBlock) + 4);
|
|
81
|
+
const constrainedUsed = constrained.used_bytes;
|
|
82
|
+
|
|
83
|
+
assert.throws(() => constrained.rotateMetadata(), { code: 'ENOSPC' }, 'rotating past total_bytes should fail with ENOSPC');
|
|
84
|
+
assert.strictEqual(constrained.used_bytes, constrainedUsed, 'a failed rotation must not consume space');
|
|
85
|
+
assert(constrained.used_bytes <= constrained.total_bytes, 'used_bytes must never exceed total_bytes');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('concurrent rotation keeps every metadata block reachable', async () => {
|
|
89
|
+
const rotations = 100;
|
|
90
|
+
const buffer = new SharedArrayBuffer(0x200000);
|
|
91
|
+
const gate = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
92
|
+
|
|
93
|
+
const superblock = new SuperBlock(buffer);
|
|
94
|
+
const original = superblock.metadata_offset;
|
|
95
|
+
|
|
96
|
+
const worker = new Worker(import.meta.dirname + '/single-buffer-rotate.worker.js', { workerData: { buffer, gate, rotations } });
|
|
97
|
+
|
|
98
|
+
const ready = Promise.withResolvers<void>();
|
|
99
|
+
const finished = Promise.withResolvers<number[]>();
|
|
100
|
+
const expired = setTimeout(() => {
|
|
101
|
+
const error = new Error('the worker did not finish rotating');
|
|
102
|
+
ready.reject(error);
|
|
103
|
+
finished.reject(error);
|
|
104
|
+
}, 1000);
|
|
105
|
+
|
|
106
|
+
worker.on('error', error => {
|
|
107
|
+
ready.reject(error);
|
|
108
|
+
finished.reject(error);
|
|
109
|
+
});
|
|
110
|
+
worker.on('message', message => (message === 'ready' ? ready.resolve() : finished.resolve(message as number[])));
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await ready.promise;
|
|
114
|
+
|
|
115
|
+
Atomics.store(gate, 0, 1);
|
|
116
|
+
Atomics.notify(gate, 0);
|
|
117
|
+
|
|
118
|
+
const mine: number[] = [];
|
|
119
|
+
for (let i = 0; i < rotations; i++) mine.push(superblock.rotateMetadata().byteOffset);
|
|
120
|
+
|
|
121
|
+
const theirs = await finished.promise;
|
|
122
|
+
|
|
123
|
+
const chain = new Set<number>();
|
|
124
|
+
for (let block: MetadataBlock | undefined = new SuperBlock(buffer).metadata; block; block = block.previous) {
|
|
125
|
+
if (chain.has(block.byteOffset)) break;
|
|
126
|
+
chain.add(block.byteOffset);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
assert.strictEqual(new Set([...mine, ...theirs]).size, rotations * 2, 'two rotations reserved the same offset');
|
|
130
|
+
assert.strictEqual(chain.size, rotations * 2 + 1, 'blocks are missing from the chain');
|
|
131
|
+
|
|
132
|
+
for (const offset of [original, ...mine, ...theirs]) assert(chain.has(offset), `the block at ${offset} is not in the chain`);
|
|
133
|
+
} finally {
|
|
134
|
+
clearTimeout(expired);
|
|
135
|
+
await worker.terminate();
|
|
136
|
+
worker.unref();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
56
140
|
test('reliability across varied file sizes', async () => {
|
|
57
141
|
const mountPoint = '/sbfs-reliability';
|
|
58
142
|
const verifyMountPoint = '/sbfs-verify';
|
|
@@ -98,4 +182,26 @@ await suite('SingleBuffer', () => {
|
|
|
98
182
|
vfs.umount(mountPoint);
|
|
99
183
|
}
|
|
100
184
|
});
|
|
185
|
+
|
|
186
|
+
test('keeps metadata aligned when files have uneven sizes #309', async () => {
|
|
187
|
+
const mountPoint = '/sbfs-rotation';
|
|
188
|
+
const buffer = new ArrayBuffer(0x400000);
|
|
189
|
+
const writable = await resolveMountConfig({ backend: SingleBuffer, buffer, label: 'rotation' });
|
|
190
|
+
mount(mountPoint, writable);
|
|
191
|
+
|
|
192
|
+
// Uneven writes leave used_bytes between alignment boundaries when the metadata block fills up.
|
|
193
|
+
const sizes = [1, 17, 257, 3, 5, 13, 1023, 4095, 7, 9];
|
|
194
|
+
try {
|
|
195
|
+
for (let i = 0; i < 400; i++) {
|
|
196
|
+
const content = Buffer.alloc(sizes[i % sizes.length], i & 0xff);
|
|
197
|
+
fs.writeFileSync(`${mountPoint}/f${i}.txt`, content);
|
|
198
|
+
}
|
|
199
|
+
for (let i = 0; i < 400; i += 37) {
|
|
200
|
+
const expected = Buffer.alloc(sizes[i % sizes.length], i & 0xff);
|
|
201
|
+
assert.deepStrictEqual(fs.readFileSync(`${mountPoint}/f${i}.txt`), expected, `content mismatch at f${i}.txt`);
|
|
202
|
+
}
|
|
203
|
+
} finally {
|
|
204
|
+
vfs.umount(mountPoint);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
101
207
|
});
|
|
@@ -78,4 +78,50 @@ suite('Context', () => {
|
|
|
78
78
|
|
|
79
79
|
assert.equal(ctx.realpathSync('.'), '/test');
|
|
80
80
|
});
|
|
81
|
+
|
|
82
|
+
test('globSync should keep the context', () => {
|
|
83
|
+
ctx.mkdirSync('/globbed');
|
|
84
|
+
ctx.writeFileSync('/globbed/inside.txt', 'in the context');
|
|
85
|
+
|
|
86
|
+
fs.mkdirSync('/globbed');
|
|
87
|
+
fs.writeFileSync('/globbed/outside.txt', 'not in the context');
|
|
88
|
+
|
|
89
|
+
const results = ctx.globSync('**/*.txt', { cwd: '/' });
|
|
90
|
+
assert(results.includes('globbed/inside.txt'), 'should match files in the context root');
|
|
91
|
+
assert(!results.includes('globbed/outside.txt'), 'should not match files outside the context root');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('glob should keep the context', async () => {
|
|
95
|
+
const results = await Array.fromAsync(ctx.promises.glob('**/*.txt', { cwd: '/' }));
|
|
96
|
+
|
|
97
|
+
assert(results.includes('globbed/inside.txt'));
|
|
98
|
+
assert(!results.includes('globbed/outside.txt'), 'should not match files outside the context root');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('glob resolves a relative cwd against the context', () => {
|
|
102
|
+
const { pwd } = context;
|
|
103
|
+
context.pwd = '/';
|
|
104
|
+
|
|
105
|
+
assert.deepEqual(ctx.globSync('*', { cwd: '/globbed' }), ['inside.txt']);
|
|
106
|
+
assert.deepEqual(ctx.globSync('*', { cwd: 'globbed' }), ['inside.txt']);
|
|
107
|
+
|
|
108
|
+
context.pwd = pwd;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('glob defaults cwd to the working directory of the context', () => {
|
|
112
|
+
const { pwd } = context;
|
|
113
|
+
context.pwd = '/globbed';
|
|
114
|
+
|
|
115
|
+
assert.deepEqual(ctx.globSync('*'), ['inside.txt']);
|
|
116
|
+
|
|
117
|
+
context.pwd = pwd;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('copyFileSync should keep the context #307', () => {
|
|
121
|
+
ctx.writeFileSync('/source.txt', 'not in real root!');
|
|
122
|
+
ctx.copyFileSync('/source.txt', '/copy.txt');
|
|
123
|
+
|
|
124
|
+
assert.equal(ctx.readFileSync('/copy.txt', 'utf-8'), 'not in real root!');
|
|
125
|
+
assert(fs.readdirSync('/ctx').includes('copy.txt'));
|
|
126
|
+
});
|
|
81
127
|
});
|
|
@@ -13,6 +13,8 @@ fs.writeFileSync('/glob/c.js', 'c');
|
|
|
13
13
|
fs.writeFileSync('/glob/sub/d.txt', 'd');
|
|
14
14
|
fs.writeFileSync('/glob/sub/e.js', 'e');
|
|
15
15
|
fs.writeFileSync('/glob/sub/deep/f.txt', 'f');
|
|
16
|
+
fs.symlinkSync('sub', '/glob/link');
|
|
17
|
+
fs.symlinkSync('/glob/nowhere', '/glob/dangling');
|
|
16
18
|
|
|
17
19
|
suite('globSync', () => {
|
|
18
20
|
test('wildcard in root', () => {
|
|
@@ -82,6 +84,66 @@ suite('globSync', () => {
|
|
|
82
84
|
assert(!results.includes('glob/c.js'), 'should exclude .js files');
|
|
83
85
|
assert(results.includes('glob/a.txt'), 'should still include .txt files');
|
|
84
86
|
});
|
|
87
|
+
|
|
88
|
+
test('cwd option', () => {
|
|
89
|
+
const results = fs.globSync('*', { cwd: '/glob' });
|
|
90
|
+
assert(results.includes('a.txt'), 'results should be relative to cwd');
|
|
91
|
+
assert(results.includes('sub'));
|
|
92
|
+
assert(!results.includes('glob/a.txt'), 'results should not be relative to the root');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('patterns are relative to cwd', () => {
|
|
96
|
+
assert.deepEqual(fs.globSync('*.txt', { cwd: '/glob/sub' }), ['d.txt']);
|
|
97
|
+
assert.deepEqual(fs.globSync('deep/*.txt', { cwd: '/glob/sub' }), ['deep/f.txt']);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('absolute patterns are relative to cwd', () => {
|
|
101
|
+
assert.deepEqual(fs.globSync('/glob/sub/*.txt', { cwd: '/glob' }), ['sub/d.txt']);
|
|
102
|
+
assert.deepEqual(fs.globSync('/glob/sub/*.txt', { cwd: '/glob/sub' }), ['d.txt']);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('exclude receives paths relative to cwd', () => {
|
|
106
|
+
const seen: string[] = [];
|
|
107
|
+
fs.globSync('*', {
|
|
108
|
+
cwd: '/glob',
|
|
109
|
+
exclude: path => {
|
|
110
|
+
seen.push(path);
|
|
111
|
+
return false;
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
assert(seen.includes('a.txt'), 'should be given cwd-relative paths');
|
|
115
|
+
assert(!seen.some(p => p.startsWith('/')), 'should not be given absolute paths');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('globstar does not follow symlinks by default', () => {
|
|
119
|
+
const results = fs.globSync('/glob/**/*.txt');
|
|
120
|
+
assert(results.includes('glob/sub/d.txt'), 'should include the real directory');
|
|
121
|
+
assert(!results.includes('glob/link/d.txt'), 'should not descend into a symlinked directory');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('followSymlinks option', () => {
|
|
125
|
+
const results = fs.globSync('/glob/**/*.txt', { followSymlinks: true });
|
|
126
|
+
assert(results.includes('glob/link/d.txt'), 'should descend into a symlinked directory');
|
|
127
|
+
assert(results.includes('glob/link/deep/f.txt'), 'should descend recursively');
|
|
128
|
+
assert(results.includes('glob/sub/d.txt'), 'should still include the real directory');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('a symlink named by the pattern is always descended into', () => {
|
|
132
|
+
const results = fs.globSync('/glob/link/*.txt');
|
|
133
|
+
assert(results.includes('glob/link/d.txt'), 'should not need followSymlinks when the pattern names the link');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('the symlink itself is still matched', () => {
|
|
137
|
+
for (const followSymlinks of [false, true]) {
|
|
138
|
+
const results = fs.globSync('/glob/*', { followSymlinks });
|
|
139
|
+
assert(results.includes('glob/link'), `should match the link itself (followSymlinks: ${followSymlinks})`);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('dangling symlinks do not throw', () => {
|
|
144
|
+
const results = fs.globSync('/glob/**', { followSymlinks: true });
|
|
145
|
+
assert(results.includes('glob/dangling'), 'should match a dangling link without following it');
|
|
146
|
+
});
|
|
85
147
|
});
|
|
86
148
|
|
|
87
149
|
await suite('promises.glob', () => {
|
|
@@ -132,4 +194,27 @@ await suite('promises.glob', () => {
|
|
|
132
194
|
const results = await Array.fromAsync(fs.promises.glob('/glob/*.xyz'));
|
|
133
195
|
assert.equal(results.length, 0);
|
|
134
196
|
});
|
|
197
|
+
|
|
198
|
+
test('globstar does not follow symlinks by default', async () => {
|
|
199
|
+
const results = await Array.fromAsync(fs.promises.glob('/glob/**/*.txt'));
|
|
200
|
+
assert(results.includes('glob/sub/d.txt'));
|
|
201
|
+
assert(!results.includes('glob/link/d.txt'), 'should not descend into a symlinked directory');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('followSymlinks option', async () => {
|
|
205
|
+
const results = await Array.fromAsync(fs.promises.glob('/glob/**/*.txt', { followSymlinks: true }));
|
|
206
|
+
assert(results.includes('glob/link/d.txt'));
|
|
207
|
+
assert(results.includes('glob/link/deep/f.txt'));
|
|
208
|
+
assert(results.includes('glob/sub/d.txt'));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('a symlink named by the pattern is always descended into', async () => {
|
|
212
|
+
const results = await Array.fromAsync(fs.promises.glob('/glob/link/*.txt'));
|
|
213
|
+
assert(results.includes('glob/link/d.txt'));
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('dangling symlinks do not throw', async () => {
|
|
217
|
+
const results = await Array.fromAsync(fs.promises.glob('/glob/**', { followSymlinks: true }));
|
|
218
|
+
assert(results.includes('glob/dangling'));
|
|
219
|
+
});
|
|
135
220
|
});
|