Skip to content
Draft
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
22 changes: 22 additions & 0 deletions lib/internal/vfs/file_handle.js
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ class MemoryFileHandle extends VirtualFileHandle {
#size;
#entry;
#getStats;
#published; // The entry content this handle last saw or wrote.

#checkClosed(syscall) {
if (this.closed) {
Expand All @@ -450,6 +451,7 @@ class MemoryFileHandle extends VirtualFileHandle {
this.#size = content.length;
this.#entry = entry;
this.#getStats = getStats;
this.#published = content;

// O_TRUNC empties the file at open time. O_APPEND does not move the
// read offset: it only forces writes to the end, so the position stays
Expand All @@ -459,10 +461,24 @@ class MemoryFileHandle extends VirtualFileHandle {
this.#size = 0;
if (entry) {
entry.content = this.#content;
this.#published = this.#content;
}
}
}

/**
* Adopts the entry's content if another handle replaced it, since an
* open descriptor is a live view of the file, not a snapshot.
*/
#sync() {
const entry = this.#entry;
if (entry && entry.content !== this.#published) {
this.#content = entry.content;
this.#size = entry.content.length;
this.#published = entry.content;
}
}

/**
* Throws EBADF if the handle was not opened for writing.
*/
Expand Down Expand Up @@ -499,6 +515,7 @@ class MemoryFileHandle extends VirtualFileHandle {
if (this.#entry?.isDynamic && this.#entry.isDynamic()) {
return this.#entry.getContentSync();
}
this.#sync();
return this.#content.subarray(0, this.#size);
}

Expand Down Expand Up @@ -572,6 +589,7 @@ class MemoryFileHandle extends VirtualFileHandle {
writeSync(buffer, offset, length, position) {
this.#checkClosed('write');
this.#checkWritable();
this.#sync();

// In append mode, always write at the end
const useCurrentPosition = isCurrentPosition(position);
Expand Down Expand Up @@ -603,6 +621,7 @@ class MemoryFileHandle extends VirtualFileHandle {
this.#entry.content = this.#content.subarray(0, this.#size);
this.#entry.mtime = now;
this.#entry.ctime = now;
this.#published = this.#entry.content;
}

// Update position if not using explicit position
Expand Down Expand Up @@ -693,6 +712,7 @@ class MemoryFileHandle extends VirtualFileHandle {
statSync(options) {
this.#checkClosed('fstat');
if (this.#getStats) {
this.#sync();
return this.#getStats(this.#size);
}
throw new ERR_INVALID_STATE('stats not available');
Expand Down Expand Up @@ -741,6 +761,7 @@ class MemoryFileHandle extends VirtualFileHandle {
truncateSync(len = 0) {
this.#checkClosed('ftruncate');
this.#checkWritable();
this.#sync();

if (len < this.#size) {
// Zero out truncated region to avoid stale data
Expand All @@ -764,6 +785,7 @@ class MemoryFileHandle extends VirtualFileHandle {
this.#entry.content = this.#content.subarray(0, this.#size);
this.#entry.mtime = now;
this.#entry.ctime = now;
this.#published = this.#entry.content;
}
}

Expand Down
94 changes: 94 additions & 0 deletions test/parallel/test-vfs-memory-handle-live-view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Flags: --experimental-vfs
'use strict';
// MemoryProvider: an open handle is a live view of the file, not a snapshot
// taken at open time. Writes made through the provider or through another
// handle must be visible to handles that are already open, as they are on a
// real filesystem. Refs: https://github.com/nodejs/node/issues/66355
const common = require('../common');
const assert = require('assert');
const { create } = require('node:vfs');

// Content replaced through the provider is seen by an open read handle.
{
const { provider: p } = create();
p.writeFileSync('/f', 'AAAA');
const h = p.openSync('/f', 'r');
p.writeFileSync('/f', '0123456789');

const b = Buffer.alloc(10);
assert.strictEqual(h.readSync(b, 0, 10, 0), 10);
assert.strictEqual(b.toString(), '0123456789');
assert.strictEqual(h.readFileSync('utf8'), '0123456789');
assert.strictEqual(h.statSync().size, 10);
assert.strictEqual(p.statSync('/f').size, 10);
h.readFile('utf8').then(common.mustCall((s) => {
assert.strictEqual(s, '0123456789');
h.closeSync();
}));
}

// A shrinking rewrite is seen too: reads stop at the new end.
{
const { provider: p } = create();
p.writeFileSync('/f', '0123456789');
const h = p.openSync('/f', 'r');
p.writeFileSync('/f', 'AB');

const b = Buffer.alloc(10);
assert.strictEqual(h.readSync(b, 0, 10, 0), 2);
assert.strictEqual(b.subarray(0, 2).toString(), 'AB');
assert.strictEqual(h.readFileSync('utf8'), 'AB');
assert.strictEqual(h.statSync().size, 2);
h.closeSync();
}

// Two writable handles on the same file do not clobber each other.
{
const { provider: p } = create();
p.writeFileSync('/f', 'AAAA');
const h1 = p.openSync('/f', 'r+');
const h2 = p.openSync('/f', 'r+');

h1.writeSync(Buffer.from('XY'), 0, 2, 0);
h2.writeSync(Buffer.from('Z'), 0, 1, 3);
assert.strictEqual(p.readFileSync('/f', 'utf8'), 'XYAZ');
assert.strictEqual(h1.readFileSync('utf8'), 'XYAZ');

// Growing through one handle is visible to the other.
h1.writeSync(Buffer.from('0123456789'), 0, 10, 4);
assert.strictEqual(h2.statSync().size, 14);
assert.strictEqual(h2.readFileSync('utf8'), 'XYAZ0123456789');

// Truncating through one handle is visible to the other.
h2.truncateSync(3);
assert.strictEqual(h1.statSync().size, 3);
assert.strictEqual(h1.readFileSync('utf8'), 'XYA');

h1.closeSync();
h2.closeSync();
}

// A write through a handle opened before the file was replaced lands in the
// current content instead of resurrecting the old bytes.
{
const { provider: p } = create();
p.writeFileSync('/f', 'AAAA');
const h = p.openSync('/f', 'r+');
p.writeFileSync('/f', 'BB');

h.writeSync(Buffer.from('C'), 0, 1, 3);
assert.deepStrictEqual(p.readFileSync('/f'), Buffer.from('BB\0C', 'latin1'));
h.closeSync();
}

// An append handle opened earlier appends after the current end.
{
const { provider: p } = create();
p.writeFileSync('/f', 'AAAA');
const h = p.openSync('/f', 'a');
p.writeFileSync('/f', 'B');

h.writeSync(Buffer.from('C'), 0, 1, null);
assert.strictEqual(p.readFileSync('/f', 'utf8'), 'BC');
h.closeSync();
}
Loading