@zenfs/core 2.6.4 → 2.6.6
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.js +15 -6
- package/dist/backends/fetch.d.ts +4 -0
- package/dist/backends/fetch.js +30 -1
- package/dist/backends/store/fs.js +14 -1
- package/dist/internal/file_index.d.ts +2 -0
- package/dist/internal/file_index.js +11 -2
- package/dist/polyfills.js +4 -1
- package/dist/vfs/file.js +2 -1
- package/dist/vfs/vcache.js +3 -0
- package/package.json +1 -5
- package/scripts/make-index.js +22 -7
- package/tests/backend/cow.test.ts +33 -0
- package/tests/backend/fetch.test.ts +8 -0
- package/tests/backend/single-buffer.test.ts +1 -1
- package/tests/backend/single-buffer.worker.js +1 -1
- package/tests/common/file_index.test.ts +51 -0
- package/tests/common/inode.test.ts +1 -1
- package/tests/fs/truncate.test.ts +22 -0
package/dist/backends/cow.js
CHANGED
|
@@ -11,7 +11,8 @@ const journalOperations = new Set(['delete']);
|
|
|
11
11
|
function isJournalOp(op) {
|
|
12
12
|
return journalOperations.has(op);
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
// @todo use `Iterator` helper #296
|
|
15
|
+
const maxOpLength = [...journalOperations].reduce((max, op) => Math.max(max, op.length), 0);
|
|
15
16
|
const journalMagicString = '#journal@v0\n';
|
|
16
17
|
/**
|
|
17
18
|
* Tracks various operations for the CoW backend
|
|
@@ -116,11 +117,19 @@ export class CopyOnWriteFS extends FileSystem {
|
|
|
116
117
|
_nextIno = 1;
|
|
117
118
|
/** Moves the public inode numbers for a rename, including a directory's descendants */
|
|
118
119
|
renameInos(oldPath, newPath) {
|
|
119
|
-
|
|
120
|
+
const changes = [];
|
|
121
|
+
for (const [path, ino] of this.inos) {
|
|
120
122
|
if (path != oldPath && !path.startsWith(oldPath + '/'))
|
|
121
123
|
continue;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
changes.push({
|
|
125
|
+
path,
|
|
126
|
+
add: newPath + path.slice(oldPath.length),
|
|
127
|
+
ino,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
for (const change of changes) {
|
|
131
|
+
this.inos.delete(change.path);
|
|
132
|
+
this.inos.set(change.add, change.ino);
|
|
124
133
|
}
|
|
125
134
|
}
|
|
126
135
|
/** Replaces the layer's inode number with the public one for `path` */
|
|
@@ -442,7 +451,7 @@ export class CopyOnWriteFS extends FileSystem {
|
|
|
442
451
|
if (size)
|
|
443
452
|
this.readable.readSync(path, data, 0, size);
|
|
444
453
|
this.writable.createFileSync(path, stats);
|
|
445
|
-
this.writable.touchSync(path, stats);
|
|
454
|
+
this.writable.touchSync(path, { ...pick(stats, _inode_fields), attributes: stats.attributes, size });
|
|
446
455
|
if (size)
|
|
447
456
|
this.writable.writeSync(path, data, 0);
|
|
448
457
|
}
|
|
@@ -460,7 +469,7 @@ export class CopyOnWriteFS extends FileSystem {
|
|
|
460
469
|
if (size)
|
|
461
470
|
await this.readable.read(path, data, 0, size);
|
|
462
471
|
await this.writable.createFile(path, stats);
|
|
463
|
-
await this.writable.touch(path, stats);
|
|
472
|
+
await this.writable.touch(path, { ...pick(stats, _inode_fields), attributes: stats.attributes, size });
|
|
464
473
|
if (size)
|
|
465
474
|
await this.writable.write(path, data, 0);
|
|
466
475
|
}
|
package/dist/backends/fetch.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { IndexData } from '../internal/file_index.js';
|
|
2
2
|
import { Index } from '../internal/file_index.js';
|
|
3
3
|
import { IndexFS } from '../internal/index_fs.js';
|
|
4
|
+
import type { Inode, InodeLike } from '../internal/inode.js';
|
|
4
5
|
import type { SharedConfig } from './backend.js';
|
|
5
6
|
/**
|
|
6
7
|
* Configuration options for FetchFS.
|
|
@@ -45,6 +46,9 @@ export declare class FetchFS extends IndexFS {
|
|
|
45
46
|
*/
|
|
46
47
|
protected _async(p: Promise<unknown>): void;
|
|
47
48
|
constructor(index: Index, baseUrl: string, requestInit?: RequestInit, remoteWrite?: boolean | undefined);
|
|
49
|
+
protected resize(path: string, inode: Inode, oldSize: number): Promise<void> | void;
|
|
50
|
+
touch(path: string, metadata: InodeLike): Promise<void>;
|
|
51
|
+
touchSync(path: string, metadata: InodeLike): void;
|
|
48
52
|
protected remove(path: string): Promise<void>;
|
|
49
53
|
protected removeSync(path: string): void;
|
|
50
54
|
read(path: string, buffer: Uint8Array, offset: number | undefined, end: number): Promise<void>;
|
package/dist/backends/fetch.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
2
|
import { withErrno } from 'kerium';
|
|
3
3
|
import { debug, err, warn } from 'kerium/log';
|
|
4
|
-
import { decodeUTF8 } from 'utilium';
|
|
4
|
+
import { _throw, decodeUTF8 } from 'utilium';
|
|
5
|
+
import { Resource } from 'utilium/cache';
|
|
5
6
|
import * as requests from 'utilium/requests';
|
|
6
7
|
import { Index } from '../internal/file_index.js';
|
|
7
8
|
import { IndexFS } from '../internal/index_fs.js';
|
|
9
|
+
import { isDirectory } from '../internal/inode.js';
|
|
8
10
|
import { normalizePath } from '../utils.js';
|
|
9
11
|
import { S_IFREG } from '../constants.js';
|
|
10
12
|
/** Parse and throw */
|
|
@@ -48,6 +50,33 @@ export class FetchFS extends IndexFS {
|
|
|
48
50
|
this.requestInit = requestInit;
|
|
49
51
|
this.remoteWrite = remoteWrite;
|
|
50
52
|
}
|
|
53
|
+
resize(path, inode, oldSize) {
|
|
54
|
+
if (isDirectory(inode) || inode.size == oldSize)
|
|
55
|
+
return;
|
|
56
|
+
const url = this.baseUrl + path;
|
|
57
|
+
const resource = requests.resourcesCache.get(url) ?? new Resource(url, oldSize, {}, requests.resourcesCache);
|
|
58
|
+
resource.size = inode.size;
|
|
59
|
+
if (inode.size < oldSize)
|
|
60
|
+
return;
|
|
61
|
+
const zeroes = new Uint8Array(inode.size - oldSize);
|
|
62
|
+
resource.add(zeroes, oldSize);
|
|
63
|
+
if (this.remoteWrite)
|
|
64
|
+
return requests.set(url, zeroes, { offset: oldSize, warn }, this.requestInit).catch(parseError);
|
|
65
|
+
}
|
|
66
|
+
async touch(path, metadata) {
|
|
67
|
+
const inode = this.index.get(path) ?? _throw(withErrno('ENOENT'));
|
|
68
|
+
const oldSize = inode.size;
|
|
69
|
+
inode.update(metadata);
|
|
70
|
+
await this.resize(path, inode, oldSize);
|
|
71
|
+
}
|
|
72
|
+
touchSync(path, metadata) {
|
|
73
|
+
const inode = this.index.get(path) ?? _throw(withErrno('ENOENT'));
|
|
74
|
+
const oldSize = inode.size;
|
|
75
|
+
inode.update(metadata);
|
|
76
|
+
const pending = this.resize(path, inode, oldSize);
|
|
77
|
+
if (pending)
|
|
78
|
+
this._async(pending);
|
|
79
|
+
}
|
|
51
80
|
async remove(path) {
|
|
52
81
|
await requests.remove(this.baseUrl + path, { warn, cacheOnly: !this.remoteWrite }, this.requestInit);
|
|
53
82
|
}
|
|
@@ -418,6 +418,12 @@ export class StoreFS extends FileSystem {
|
|
|
418
418
|
try {
|
|
419
419
|
const tx = __addDisposableResource(env_9, this.transaction(), true);
|
|
420
420
|
const inode = await this.findInode(tx, path);
|
|
421
|
+
if (!isDirectory(inode) && metadata.size !== undefined && metadata.size !== inode.size) {
|
|
422
|
+
const data = (await tx.get(inode.data)) ?? new Uint8Array();
|
|
423
|
+
if (metadata.size < data.byteLength)
|
|
424
|
+
data.fill(0, metadata.size);
|
|
425
|
+
await tx.set(inode.data, metadata.size < data.byteLength ? data.subarray(0, metadata.size) : extendBuffer(data, metadata.size));
|
|
426
|
+
}
|
|
421
427
|
if (inode.update(metadata)) {
|
|
422
428
|
this._add(inode.ino, path);
|
|
423
429
|
tx.setSync(inode.ino, inode);
|
|
@@ -439,6 +445,12 @@ export class StoreFS extends FileSystem {
|
|
|
439
445
|
try {
|
|
440
446
|
const tx = __addDisposableResource(env_10, this.transaction(), false);
|
|
441
447
|
const inode = this.findInodeSync(tx, path);
|
|
448
|
+
if (!isDirectory(inode) && metadata.size !== undefined && metadata.size !== inode.size) {
|
|
449
|
+
const data = tx.getSync(inode.data) ?? new Uint8Array();
|
|
450
|
+
if (metadata.size < data.byteLength)
|
|
451
|
+
data.fill(0, metadata.size);
|
|
452
|
+
tx.setSync(inode.data, metadata.size < data.byteLength ? data.subarray(0, metadata.size) : extendBuffer(data, metadata.size));
|
|
453
|
+
}
|
|
442
454
|
if (inode.update(metadata)) {
|
|
443
455
|
this._add(inode.ino, path);
|
|
444
456
|
tx.setSync(inode.ino, inode);
|
|
@@ -921,7 +933,8 @@ export class StoreFS extends FileSystem {
|
|
|
921
933
|
_lastID;
|
|
922
934
|
/** Allocates a new ID and adds the ID/path */
|
|
923
935
|
allocNew(path) {
|
|
924
|
-
|
|
936
|
+
// @todo use `Iterator` helper #296
|
|
937
|
+
this._lastID ||= Array.from(this._paths.keys()).reduce((max, ino) => Math.max(max, ino), 0);
|
|
925
938
|
this._lastID += 2;
|
|
926
939
|
const id = this._lastID;
|
|
927
940
|
if (id > size_max)
|
|
@@ -17,6 +17,7 @@ export declare const version = 1;
|
|
|
17
17
|
* @internal
|
|
18
18
|
*/
|
|
19
19
|
export declare class Index extends Map<string, Inode> {
|
|
20
|
+
#private;
|
|
20
21
|
maxSize: number;
|
|
21
22
|
/**
|
|
22
23
|
* Converts the index to JSON
|
|
@@ -26,6 +27,7 @@ export declare class Index extends Map<string, Inode> {
|
|
|
26
27
|
* Converts the index to a string
|
|
27
28
|
*/
|
|
28
29
|
toString(): string;
|
|
30
|
+
clear(): void;
|
|
29
31
|
/**
|
|
30
32
|
* Get the size in bytes of the index (including the size reported for each entry)
|
|
31
33
|
*/
|
|
@@ -21,7 +21,8 @@ export class Index extends Map {
|
|
|
21
21
|
return {
|
|
22
22
|
version,
|
|
23
23
|
maxSize: this.maxSize,
|
|
24
|
-
|
|
24
|
+
// @todo use `Iterator.map` #296
|
|
25
|
+
entries: Object.fromEntries(Array.from(this).map(([k, v]) => [k, v.toJSON()])),
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
28
|
/**
|
|
@@ -30,6 +31,10 @@ export class Index extends Map {
|
|
|
30
31
|
toString() {
|
|
31
32
|
return JSON.stringify(this.toJSON());
|
|
32
33
|
}
|
|
34
|
+
clear() {
|
|
35
|
+
super.clear();
|
|
36
|
+
this.#lastAlloc = 0;
|
|
37
|
+
}
|
|
33
38
|
/**
|
|
34
39
|
* Get the size in bytes of the index (including the size reported for each entry)
|
|
35
40
|
*/
|
|
@@ -74,12 +79,16 @@ export class Index extends Map {
|
|
|
74
79
|
}
|
|
75
80
|
return entries;
|
|
76
81
|
}
|
|
82
|
+
#lastAlloc = 0;
|
|
77
83
|
/**
|
|
78
84
|
* Get the next available ID in the index
|
|
79
85
|
* @internal
|
|
80
86
|
*/
|
|
81
87
|
_alloc() {
|
|
82
|
-
|
|
88
|
+
// @todo use `Iterator.flatMap` #296
|
|
89
|
+
this.#lastAlloc ||= Array.from(this.values()).reduce((max, inode) => Math.max(max, inode.ino, inode.data), 0);
|
|
90
|
+
this.#lastAlloc++;
|
|
91
|
+
return this.#lastAlloc++;
|
|
83
92
|
}
|
|
84
93
|
/**
|
|
85
94
|
* Gets a list of entries for each directory in the index.
|
package/dist/polyfills.js
CHANGED
|
@@ -43,7 +43,10 @@ Uint8Array.fromBase64 ??=
|
|
|
43
43
|
Uint8Array.prototype.toHex ??=
|
|
44
44
|
(warn('Using a polyfill of Uint8Array.prototype.toHex'),
|
|
45
45
|
function toHex() {
|
|
46
|
-
|
|
46
|
+
// @todo use `Iterator.map` #296
|
|
47
|
+
return Array.from(this)
|
|
48
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
49
|
+
.join('');
|
|
47
50
|
});
|
|
48
51
|
Uint8Array.fromHex ??=
|
|
49
52
|
(warn('Using a polyfill of Uint8Array.fromHex'),
|
package/dist/vfs/file.js
CHANGED
|
@@ -675,7 +675,8 @@ export { Handle };
|
|
|
675
675
|
*/
|
|
676
676
|
export function toFD(file) {
|
|
677
677
|
const map = contextOf(file.context).descriptors;
|
|
678
|
-
|
|
678
|
+
// @todo use `Iterator` helper #296
|
|
679
|
+
const fd = Array.from(map.keys()).reduce((max, k) => Math.max(max, k), 4);
|
|
679
680
|
map.set(fd, file);
|
|
680
681
|
return fd;
|
|
681
682
|
}
|
package/dist/vfs/vcache.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "2.6.6",
|
|
4
4
|
"description": "A filesystem, anywhere",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -90,12 +90,8 @@
|
|
|
90
90
|
"eslint": "^10.1.0",
|
|
91
91
|
"globals": "^17.3.0",
|
|
92
92
|
"prettier": "^3.2.5",
|
|
93
|
-
"tsx": "^4.19.1",
|
|
94
93
|
"typedoc": "^0.28.18",
|
|
95
94
|
"typescript": "^6.0.0",
|
|
96
95
|
"typescript-eslint": "^8.58.0"
|
|
97
|
-
},
|
|
98
|
-
"allowScripts": {
|
|
99
|
-
"esbuild": true
|
|
100
96
|
}
|
|
101
97
|
}
|
package/scripts/make-index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { lstatSync, readdirSync, writeFileSync
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 () => {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
import { Index, Inode } from '@zenfs/core';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { suite, test } from 'node:test';
|
|
5
|
+
|
|
6
|
+
/** Build an index with `n` file entries, using IDs 2..2n+1 */
|
|
7
|
+
function buildIndex(n: number): Index {
|
|
8
|
+
const index = new Index();
|
|
9
|
+
index.set('/', new Inode({ ino: 0, data: 1, mode: 0o40755, nlink: 1 }));
|
|
10
|
+
for (let i = 0; i < n; i++) {
|
|
11
|
+
index.set(`/f${i}`, new Inode({ ino: 2 * i + 2, data: 2 * i + 3, mode: 0o100644, nlink: 1 }));
|
|
12
|
+
}
|
|
13
|
+
return index;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Calls `fn` with `depth` extra frames on the stack */
|
|
17
|
+
function atDepth<T>(depth: number, fn: () => T): T {
|
|
18
|
+
return depth > 0 ? atDepth(depth - 1, fn) : fn();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
suite('Index', () => {
|
|
22
|
+
test('_alloc returns an unused ID', () => {
|
|
23
|
+
const index = buildIndex(10);
|
|
24
|
+
assert.equal(index._alloc(), 22);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('_alloc does not re-use IDs', () => {
|
|
28
|
+
const index = buildIndex(10);
|
|
29
|
+
const ids = new Set<number>();
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < 100; i++) {
|
|
32
|
+
const id = index._alloc();
|
|
33
|
+
assert.equal(index.getByID(id), undefined, `\`_alloc\` returned in-use ID ${id}`);
|
|
34
|
+
assert(!ids.has(id), `\`_alloc\` returned duplicate ID ${id}`);
|
|
35
|
+
ids.add(id);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('_alloc works with a large index #312', () => {
|
|
40
|
+
const index = buildIndex(100_000);
|
|
41
|
+
assert.equal(index._alloc(), 200_002);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('_alloc works with a large index and a deep stack #312', () => {
|
|
45
|
+
const index = buildIndex(60_000);
|
|
46
|
+
assert.equal(
|
|
47
|
+
atDepth(1000, () => index._alloc()),
|
|
48
|
+
120_002
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -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
|
});
|