Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions lib/internal/vfs/file_handle.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const {
} = require('internal/errors');
const {
createEBADF,
createEROFS,
} = require('internal/vfs/errors');

// Private symbols
Expand Down Expand Up @@ -365,6 +366,7 @@ class MemoryFileHandle extends VirtualFileHandle {
#size;
#entry;
#getStats;
#isReadOnly;

#checkClosed(syscall) {
if (this.closed) {
Expand All @@ -379,13 +381,15 @@ class MemoryFileHandle extends VirtualFileHandle {
* @param {Buffer} content The initial file content
* @param {object} entry The entry object (for updating content)
* @param {Function} getStats Function to get updated stats
* @param {Function} [isReadOnly] Returns the provider's read-only state
*/
constructor(path, flags, mode, content, entry, getStats) {
constructor(path, flags, mode, content, entry, getStats, isReadOnly) {
super(path, flags, mode);
this.#content = content;
this.#size = content.length;
this.#entry = entry;
this.#getStats = getStats;
this.#isReadOnly = isReadOnly;

// Handle different open modes
if (flags === 'w' || flags === 'w+' ||
Expand All @@ -404,11 +408,16 @@ class MemoryFileHandle extends VirtualFileHandle {
}

/**
* Throws EBADF if the handle was not opened for writing.
* Throws EBADF if the handle was not opened for writing, or EROFS if the
* provider is read-only.
* @param {string} syscall The system call name
*/
#checkWritable() {
#checkWritable(syscall) {
if (this.flags === 'r') {
throw createEBADF('write');
throw createEBADF(syscall);
}
if (this.#isReadOnly?.()) {
throw createEROFS(syscall, this.path);
}
}

Expand Down Expand Up @@ -513,7 +522,7 @@ class MemoryFileHandle extends VirtualFileHandle {
*/
writeSync(buffer, offset, length, position) {
this.#checkClosed('write');
this.#checkWritable();
this.#checkWritable('write');

// In append mode, always write at the end
const useCurrentPosition = isCurrentPosition(position);
Expand Down Expand Up @@ -612,7 +621,7 @@ class MemoryFileHandle extends VirtualFileHandle {
*/
writeFileSync(data, options) {
this.#checkClosed('write');
this.#checkWritable();
this.#checkWritable('write');

const buffer = typeof data === 'string' ? Buffer.from(data, options?.encoding) : data;

Expand Down Expand Up @@ -681,7 +690,7 @@ class MemoryFileHandle extends VirtualFileHandle {
*/
truncateSync(len = 0) {
this.#checkClosed('ftruncate');
this.#checkWritable();
this.#checkWritable('ftruncate');

if (len < this.#size) {
// Zero out truncated region to avoid stale data
Expand Down
11 changes: 10 additions & 1 deletion lib/internal/vfs/providers/memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,16 @@ class MemoryProvider extends VirtualProvider {
}

const getStats = (size) => this.#createStats(entry, size);
return new MemoryFileHandle(normalized, flags, mode ?? entry.mode, entry.content, entry, getStats);
const isReadOnly = () => this.readonly;
return new MemoryFileHandle(
normalized,
flags,
mode ?? entry.mode,
entry.content,
entry,
getStats,
isReadOnly,
);
}

async open(path, flags, mode) {
Expand Down
43 changes: 43 additions & 0 deletions test/parallel/test-vfs-memory-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,8 @@ const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'content');
myVfs.mkdirSync('/dir', { recursive: true });
const fd = myVfs.openSync('/file.txt', 'r+');
const appendFd = myVfs.openSync('/file.txt', 'a');

// Set to readonly
myVfs.provider.setReadOnly();
Expand Down Expand Up @@ -629,12 +631,30 @@ const vfs = require('node:vfs');
assert.throws(() => {
myVfs.symlinkSync('/file.txt', '/link');
}, { code: 'EROFS' });

// File descriptors opened before setReadOnly() must not bypass it.
assert.throws(() => {
myVfs.writeSync(fd, Buffer.from('new'), 0, 3, 0);
}, { code: 'EROFS' });

assert.throws(() => {
myVfs.ftruncateSync(fd, 0);
}, { code: 'EROFS' });

assert.throws(() => {
myVfs.writeSync(appendFd, Buffer.from('new'), 0, 3, null);
}, { code: 'EROFS' });

assert.strictEqual(myVfs.readFileSync('/file.txt', 'utf8'), 'content');
myVfs.closeSync(fd);
myVfs.closeSync(appendFd);
}

// Test async operations on readonly MemoryProvider
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/readonly.txt', 'content');
const handle = await myVfs.provider.open('/readonly.txt', 'r+');
myVfs.provider.setReadOnly();

await assert.rejects(
Expand All @@ -661,4 +681,27 @@ const vfs = require('node:vfs');
myVfs.promises.copyFile('/readonly.txt', '/copy.txt'),
{ code: 'EROFS' }
);

await assert.rejects(
handle.write(Buffer.from('new'), 0, 3, 0),
{ code: 'EROFS' }
);

await assert.rejects(
handle.writeFile('new'),
{ code: 'EROFS' }
);

await assert.rejects(
handle.truncate(0),
{ code: 'EROFS' }
);

await assert.rejects(
handle.writev([Buffer.from('n'), Buffer.from('ew')], 0),
{ code: 'EROFS' }
);

assert.strictEqual(await handle.readFile('utf8'), 'content');
await handle.close();
})().then(common.mustCall());
Loading