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
107 changes: 62 additions & 45 deletions lib/internal/vfs/file_handle.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,52 @@ const kFlags = Symbol('kFlags');
const kMode = Symbol('kMode');
const kPosition = Symbol('kPosition');
const kClosed = Symbol('kClosed');
const kAccess = Symbol('kAccess');

const { stringToFlags } = require('internal/fs/utils');
const {
fs: { O_APPEND, O_CREAT, O_EXCL, O_RDONLY, O_RDWR, O_TRUNC, O_WRONLY },
} = internalBinding('constants');

/**
* Decodes open flags into what they ask for. Both the string spellings and
* the numeric `fs.constants` values are accepted. The bits decide, because
* several numeric combinations (`O_WRONLY` alone, `O_RDONLY | O_CREAT`)
* have no string spelling, and collapsing them to the nearest one changes
* their meaning: a plain `O_WRONLY` must neither create nor truncate.
* @param {string|number} flags
* @returns {{ readable: boolean, writable: boolean, create: boolean,
* exclusive: boolean, truncate: boolean, append: boolean }}
*/
function decodeOpenFlags(flags) {
const bits = typeof flags === 'number' ? flags : stringToFlags(flags);
const access = bits & (O_RDONLY | O_WRONLY | O_RDWR);
return {
__proto__: null,
readable: access !== O_WRONLY,
writable: access !== O_RDONLY,
create: (bits & O_CREAT) !== 0,
exclusive: (bits & O_EXCL) !== 0,
truncate: (bits & O_TRUNC) !== 0,
append: (bits & O_APPEND) !== 0,
};
}

/**
* The string spelling closest to numeric flags, for `handle.flags`, which
* has always been a string. Behaviour is never derived from it.
* @param {string|number} flags
* @returns {string}
*/
function flagsToString(flags) {
if (typeof flags !== 'number') return flags;
const { readable, writable, exclusive, truncate, append } = decodeOpenFlags(flags);
const plus = readable && writable ? '+' : '';
const x = exclusive ? 'x' : '';
if (append) return `a${x}${plus}`;
if (truncate) return `w${x}${plus}`;
return writable ? 'r+' : 'r';
}

function isCurrentPosition(position) {
return position === null || position === undefined || position === -1;
Expand All @@ -44,7 +90,8 @@ class VirtualFileHandle {
*/
constructor(path, flags, mode) {
this[kPath] = path;
this[kFlags] = flags;
this[kAccess] = decodeOpenFlags(flags);
this[kFlags] = flagsToString(flags);
this[kMode] = mode ?? 0o644;
this[kPosition] = 0;
this[kClosed] = false;
Expand Down Expand Up @@ -387,27 +434,23 @@ class MemoryFileHandle extends VirtualFileHandle {
this.#entry = entry;
this.#getStats = getStats;

// Handle different open modes
if (flags === 'w' || flags === 'w+' ||
flags === 'wx' || flags === 'wx+') {
// Write mode: truncate
// 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
// at 0 and reads start from the beginning as they do on a real file.
if (this[kAccess].truncate) {
this.#content = Buffer.alloc(0);
this.#size = 0;
if (entry) {
entry.content = this.#content;
}
} else if (flags === 'a' || flags === 'a+' ||
flags === 'ax' || flags === 'ax+') {
// Append mode: position at end
this.position = this.#size;
}
}

/**
* Throws EBADF if the handle was not opened for writing.
*/
#checkWritable() {
if (this.flags === 'r') {
if (!this[kAccess].writable) {
throw createEBADF('write');
}
}
Expand All @@ -416,8 +459,7 @@ class MemoryFileHandle extends VirtualFileHandle {
* Throws EBADF if the handle was not opened for reading.
*/
#checkReadable() {
const f = this.flags;
if (f === 'w' || f === 'a' || f === 'wx' || f === 'ax') {
if (!this[kAccess].readable) {
throw createEBADF('read');
}
}
Expand All @@ -427,8 +469,7 @@ class MemoryFileHandle extends VirtualFileHandle {
* @returns {boolean}
*/
#isAppend() {
const f = this.flags;
return f === 'a' || f === 'a+' || f === 'ax' || f === 'ax+';
return this[kAccess].append;
}

/**
Expand Down Expand Up @@ -605,42 +646,16 @@ class MemoryFileHandle extends VirtualFileHandle {
}

/**
* Writes data to the file synchronously.
* Replaces content in 'w' mode, appends in 'a' mode.
* Writes data to the file synchronously, from the current position (the
* end, in append mode), the way `filehandle.writeFile()` does. Whether
* earlier content is discarded was decided by the open flags: "w" has
* already truncated, "r+" overwrites in place and keeps any tail.
* @param {Buffer|string} data The data to write
* @param {object} [options] Options
*/
writeFileSync(data, options) {
this.#checkClosed('write');
this.#checkWritable();

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

// In append mode, append to existing content
if (this.#isAppend()) {
const neededSize = this.#size + buffer.length;
if (neededSize > this.#content.length) {
const newCapacity = MathMax(neededSize, this.#content.length * 2);
const newContent = Buffer.alloc(newCapacity);
this.#content.copy(newContent, 0, 0, this.#size);
this.#content = newContent;
}
buffer.copy(this.#content, this.#size);
this.#size = neededSize;
} else {
this.#content = Buffer.from(buffer);
this.#size = buffer.length;
}

// Update the entry's content, mtime, and ctime
if (this.#entry) {
const now = DateNow();
this.#entry.content = this.#content.subarray(0, this.#size);
this.#entry.mtime = now;
this.#entry.ctime = now;
}

this.position = this.#size;
this.writeSync(buffer, 0, buffer.length, null);
}

/**
Expand Down Expand Up @@ -721,4 +736,6 @@ class MemoryFileHandle extends VirtualFileHandle {
module.exports = {
VirtualFileHandle,
MemoryFileHandle,
decodeOpenFlags,
kAccess,
};
59 changes: 8 additions & 51 deletions lib/internal/vfs/providers/memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const { Buffer } = require('buffer');
const { isPromise } = require('util/types');
const { posix: pathPosix } = require('path');
const { VirtualProvider } = require('internal/vfs/provider');
const { MemoryFileHandle } = require('internal/vfs/file_handle');
const { MemoryFileHandle, decodeOpenFlags } = require('internal/vfs/file_handle');
const {
VFSWatcher,
VFSStatWatcher,
Expand Down Expand Up @@ -46,45 +46,12 @@ const { Dirent } = require('internal/fs/utils');
const { kEmptyObject } = require('internal/util');
const {
fs: {
O_APPEND,
O_CREAT,
O_EXCL,
O_RDWR,
O_TRUNC,
O_WRONLY,
UV_DIRENT_FILE,
UV_DIRENT_DIR,
UV_DIRENT_LINK,
},
} = internalBinding('constants');

/**
* Converts numeric flags to a string representation.
* If already a string, returns as-is.
* @param {string|number} flags The flags to normalize
* @returns {string} Normalized string flags
*/
function normalizeFlags(flags) {
if (typeof flags === 'string') return flags;
if (typeof flags !== 'number') return 'r';

const rdwr = (flags & O_RDWR) !== 0;
const append = (flags & O_APPEND) !== 0;
const excl = (flags & O_EXCL) !== 0;
const write = (flags & O_WRONLY) !== 0 ||
(flags & O_CREAT) !== 0 ||
(flags & O_TRUNC) !== 0;

if (append) {
return 'a' + (excl ? 'x' : '') + (rdwr ? '+' : '');
}
if (write) {
return 'w' + (excl ? 'x' : '') + (rdwr ? '+' : '');
}
if (rdwr) return 'r+';
return 'r';
}

/**
* Converts a time argument (Date, number, or string) to milliseconds.
* Numbers are treated as seconds (matching Node.js utimes convention).
Expand Down Expand Up @@ -496,33 +463,23 @@ class MemoryProvider extends VirtualProvider {

openSync(path, flags, mode) {
const normalized = this.#normalizePath(path);
const access = decodeOpenFlags(flags);

// Normalize numeric flags to string
flags = normalizeFlags(flags);

// Handle create and exclusive modes
const isCreate = flags === 'w' || flags === 'w+' ||
flags === 'a' || flags === 'a+' ||
flags === 'wx' || flags === 'wx+' ||
flags === 'ax' || flags === 'ax+';
const isExclusive = flags === 'wx' || flags === 'wx+' ||
flags === 'ax' || flags === 'ax+';
const isWritable = flags !== 'r';

// Check readonly for any writable mode
if (this.readonly && isWritable) {
// Creating a file is a write even when the handle itself is read-only.
if (this.readonly && (access.writable || access.create)) {
throw createEROFS('open', path);
}

let entry;
try {
entry = this.#getEntry(normalized, 'open');
// Exclusive flag: file must not exist
if (isExclusive) {
// O_EXCL only means anything together with O_CREAT: the file must
// not exist yet.
if (access.create && access.exclusive) {
throw createEEXIST('open', path);
}
} catch (err) {
if (err.code !== 'ENOENT' || !isCreate) throw err;
if (err.code !== 'ENOENT' || !access.create) throw err;
// Create the file
const parent = this.#ensureParent(normalized, false, 'open');
const name = pathPosix.basename(normalized);
Expand Down
Loading
Loading