Skip to content

Commit 7c9dac5

Browse files
committed
vfs: add tests and fix appendFile, add readonly checks
- Fix MemoryFileHandle.writeFileSync to append content in 'a' mode - Add EROFS readonly checks to MemoryProvider write operations - Add new test file for MemoryProvider covering: - copyFile/copyFileSync operations - appendFile/appendFileSync operations - readonly mode enforcement on all write operations
1 parent 3de6a3f commit 7c9dac5

File tree

3 files changed

+224
-2
lines changed

3 files changed

+224
-2
lines changed

lib/internal/vfs/file_handle.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -446,15 +446,25 @@ class MemoryFileHandle extends VirtualFileHandle {
446446
}
447447

448448
/**
449-
* Writes data to the file synchronously (replacing content).
449+
* Writes data to the file synchronously.
450+
* Replaces content in 'w' mode, appends in 'a' mode.
450451
* @param {Buffer|string} data The data to write
451452
* @param {object} [options] Options
452453
*/
453454
writeFileSync(data, options) {
454455
this._checkClosed();
455456

456457
const buffer = typeof data === 'string' ? Buffer.from(data, options?.encoding) : data;
457-
this.#content = Buffer.from(buffer);
458+
459+
// In append mode, append to existing content
460+
if (this.flags === 'a' || this.flags === 'a+') {
461+
const newContent = Buffer.alloc(this.#content.length + buffer.length);
462+
this.#content.copy(newContent, 0);
463+
buffer.copy(newContent, this.#content.length);
464+
this.#content = newContent;
465+
} else {
466+
this.#content = Buffer.from(buffer);
467+
}
458468

459469
// Update the entry's content
460470
if (this.#entry) {

lib/internal/vfs/providers/memory.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const {
2323
createEEXIST,
2424
createEINVAL,
2525
createELOOP,
26+
createEROFS,
2627
} = require('internal/vfs/errors');
2728
const {
2829
createFileStats,
@@ -451,6 +452,11 @@ class MemoryProvider extends VirtualProvider {
451452
// Handle create modes
452453
const isCreate = flags === 'w' || flags === 'w+' || flags === 'a' || flags === 'a+';
453454

455+
// Check readonly for write modes
456+
if (this.readonly && isCreate) {
457+
throw createEROFS('open', path);
458+
}
459+
454460
let entry;
455461
try {
456462
entry = this._getEntry(normalized, 'open');
@@ -539,6 +545,10 @@ class MemoryProvider extends VirtualProvider {
539545
}
540546

541547
mkdirSync(path, options) {
548+
if (this.readonly) {
549+
throw createEROFS('mkdir', path);
550+
}
551+
542552
const normalized = this._normalizePath(path);
543553
const recursive = options?.recursive === true;
544554

@@ -586,6 +596,10 @@ class MemoryProvider extends VirtualProvider {
586596
}
587597

588598
rmdirSync(path) {
599+
if (this.readonly) {
600+
throw createEROFS('rmdir', path);
601+
}
602+
589603
const normalized = this._normalizePath(path);
590604
const entry = this._getEntry(normalized, 'rmdir', true);
591605

@@ -607,6 +621,10 @@ class MemoryProvider extends VirtualProvider {
607621
}
608622

609623
unlinkSync(path) {
624+
if (this.readonly) {
625+
throw createEROFS('unlink', path);
626+
}
627+
610628
const normalized = this._normalizePath(path);
611629
const entry = this._getEntry(normalized, 'unlink', false);
612630

@@ -624,6 +642,10 @@ class MemoryProvider extends VirtualProvider {
624642
}
625643

626644
renameSync(oldPath, newPath) {
645+
if (this.readonly) {
646+
throw createEROFS('rename', oldPath);
647+
}
648+
627649
const normalizedOld = this._normalizePath(oldPath);
628650
const normalizedNew = this._normalizePath(newPath);
629651

@@ -661,6 +683,10 @@ class MemoryProvider extends VirtualProvider {
661683
}
662684

663685
symlinkSync(target, path, type) {
686+
if (this.readonly) {
687+
throw createEROFS('symlink', path);
688+
}
689+
664690
const normalized = this._normalizePath(path);
665691

666692
// Check if already exists
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const fs = require('fs');
6+
7+
// Test copyFileSync
8+
{
9+
const myVfs = fs.createVirtual();
10+
myVfs.writeFileSync('/source.txt', 'original content');
11+
12+
myVfs.copyFileSync('/source.txt', '/dest.txt');
13+
assert.strictEqual(myVfs.readFileSync('/dest.txt', 'utf8'), 'original content');
14+
15+
// Source file should still exist
16+
assert.strictEqual(myVfs.existsSync('/source.txt'), true);
17+
18+
// Test copying to nested path
19+
myVfs.mkdirSync('/nested/dir', { recursive: true });
20+
myVfs.copyFileSync('/source.txt', '/nested/dir/copy.txt');
21+
assert.strictEqual(myVfs.readFileSync('/nested/dir/copy.txt', 'utf8'), 'original content');
22+
23+
// Test copying non-existent file
24+
assert.throws(() => {
25+
myVfs.copyFileSync('/nonexistent.txt', '/fail.txt');
26+
}, { code: 'ENOENT' });
27+
}
28+
29+
// Test async copyFile
30+
(async () => {
31+
const myVfs = fs.createVirtual();
32+
myVfs.writeFileSync('/async-source.txt', 'async content');
33+
34+
await myVfs.promises.copyFile('/async-source.txt', '/async-dest.txt');
35+
assert.strictEqual(myVfs.readFileSync('/async-dest.txt', 'utf8'), 'async content');
36+
37+
// Test copying non-existent file
38+
await assert.rejects(
39+
myVfs.promises.copyFile('/nonexistent.txt', '/fail.txt'),
40+
{ code: 'ENOENT' }
41+
);
42+
})().then(common.mustCall());
43+
44+
// Test copyFileSync with mode argument
45+
{
46+
const myVfs = fs.createVirtual();
47+
myVfs.writeFileSync('/src-mode.txt', 'mode content');
48+
49+
// copyFileSync also accepts a mode argument (ignored for VFS but tests the code path)
50+
myVfs.copyFileSync('/src-mode.txt', '/dest-mode.txt', 0);
51+
assert.strictEqual(myVfs.readFileSync('/dest-mode.txt', 'utf8'), 'mode content');
52+
}
53+
54+
// Test appendFileSync
55+
{
56+
const myVfs = fs.createVirtual();
57+
myVfs.writeFileSync('/append.txt', 'hello');
58+
59+
myVfs.appendFileSync('/append.txt', ' world');
60+
assert.strictEqual(myVfs.readFileSync('/append.txt', 'utf8'), 'hello world');
61+
62+
// Append more
63+
myVfs.appendFileSync('/append.txt', '!');
64+
assert.strictEqual(myVfs.readFileSync('/append.txt', 'utf8'), 'hello world!');
65+
66+
// Append to non-existent file creates it
67+
myVfs.appendFileSync('/newfile.txt', 'new content');
68+
assert.strictEqual(myVfs.readFileSync('/newfile.txt', 'utf8'), 'new content');
69+
}
70+
71+
// Test async appendFile
72+
(async () => {
73+
const myVfs = fs.createVirtual();
74+
myVfs.writeFileSync('/async-append.txt', 'start');
75+
76+
await myVfs.promises.appendFile('/async-append.txt', '-end');
77+
assert.strictEqual(myVfs.readFileSync('/async-append.txt', 'utf8'), 'start-end');
78+
})().then(common.mustCall());
79+
80+
// Test appendFileSync with Buffer
81+
{
82+
const myVfs = fs.createVirtual();
83+
myVfs.writeFileSync('/buffer-append.txt', Buffer.from('start'));
84+
85+
myVfs.appendFileSync('/buffer-append.txt', Buffer.from('-buffer'));
86+
assert.strictEqual(myVfs.readFileSync('/buffer-append.txt', 'utf8'), 'start-buffer');
87+
}
88+
89+
// Test MemoryProvider readonly mode
90+
{
91+
const myVfs = fs.createVirtual();
92+
myVfs.writeFileSync('/file.txt', 'content');
93+
myVfs.mkdirSync('/dir', { recursive: true });
94+
95+
// Set to readonly
96+
myVfs.provider.setReadOnly();
97+
assert.strictEqual(myVfs.provider.readonly, true);
98+
99+
// Read operations should still work
100+
assert.strictEqual(myVfs.readFileSync('/file.txt', 'utf8'), 'content');
101+
assert.strictEqual(myVfs.existsSync('/file.txt'), true);
102+
assert.ok(myVfs.statSync('/file.txt'));
103+
104+
// Write operations should throw EROFS
105+
assert.throws(() => {
106+
myVfs.writeFileSync('/file.txt', 'new content');
107+
}, { code: 'EROFS' });
108+
109+
assert.throws(() => {
110+
myVfs.writeFileSync('/new.txt', 'content');
111+
}, { code: 'EROFS' });
112+
113+
assert.throws(() => {
114+
myVfs.appendFileSync('/file.txt', 'more');
115+
}, { code: 'EROFS' });
116+
117+
assert.throws(() => {
118+
myVfs.mkdirSync('/newdir');
119+
}, { code: 'EROFS' });
120+
121+
assert.throws(() => {
122+
myVfs.unlinkSync('/file.txt');
123+
}, { code: 'EROFS' });
124+
125+
assert.throws(() => {
126+
myVfs.rmdirSync('/dir');
127+
}, { code: 'EROFS' });
128+
129+
assert.throws(() => {
130+
myVfs.renameSync('/file.txt', '/renamed.txt');
131+
}, { code: 'EROFS' });
132+
133+
assert.throws(() => {
134+
myVfs.copyFileSync('/file.txt', '/copy.txt');
135+
}, { code: 'EROFS' });
136+
137+
assert.throws(() => {
138+
myVfs.symlinkSync('/file.txt', '/link');
139+
}, { code: 'EROFS' });
140+
}
141+
142+
// Test async operations on readonly VFS
143+
(async () => {
144+
const myVfs = fs.createVirtual();
145+
myVfs.writeFileSync('/readonly.txt', 'content');
146+
myVfs.provider.setReadOnly();
147+
148+
await assert.rejects(
149+
myVfs.promises.writeFile('/readonly.txt', 'new'),
150+
{ code: 'EROFS' }
151+
);
152+
153+
await assert.rejects(
154+
myVfs.promises.appendFile('/readonly.txt', 'more'),
155+
{ code: 'EROFS' }
156+
);
157+
158+
await assert.rejects(
159+
myVfs.promises.mkdir('/newdir'),
160+
{ code: 'EROFS' }
161+
);
162+
163+
await assert.rejects(
164+
myVfs.promises.unlink('/readonly.txt'),
165+
{ code: 'EROFS' }
166+
);
167+
168+
await assert.rejects(
169+
myVfs.promises.copyFile('/readonly.txt', '/copy.txt'),
170+
{ code: 'EROFS' }
171+
);
172+
})().then(common.mustCall());
173+
174+
// Test accessSync
175+
{
176+
const myVfs = fs.createVirtual();
177+
myVfs.writeFileSync('/access-test.txt', 'content');
178+
179+
// Should not throw for existing file
180+
myVfs.accessSync('/access-test.txt');
181+
182+
// Should throw for non-existent file
183+
assert.throws(() => {
184+
myVfs.accessSync('/nonexistent.txt');
185+
}, { code: 'ENOENT' });
186+
}

0 commit comments

Comments
 (0)