/
githubmirror
/
node
Обзор
Документация
Войти
/
githubmirror
/
node
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
lib/internal/vfs/providers/memory.js
1 089 строк
31 KB
Trivikram Kamat
vfs: make lchown update symlink metadata
28 июл 2026, 08:46
Не верифицирован
28 июл 2026, 08:46
b2a024b
Код
Авторство
О чём код?
'use strict'; const { ArrayFrom, ArrayPrototypePop, ArrayPrototypePush, DateNow, SafeMap, SafeSet, StringPrototypeReplaceAll, StringPrototypeStartsWith, Symbol, } = primordials; 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 { VFSWatcher, VFSStatWatcher, VFSWatchAsyncIterable, } = require('internal/vfs/watcher'); const { codes: { ERR_INVALID_STATE, }, } = require('internal/errors'); const { createENOENT, createENOTDIR, createENOTEMPTY, createEISDIR, createEEXIST, createEINVAL, createELOOP, createEROFS, } = require('internal/vfs/errors'); const { createFileStats, createDirectoryStats, createSymlinkStats, } = require('internal/vfs/stats'); 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). * @param {Date|number|string} time The time value * @returns {number} Milliseconds since epoch */ function toMs(time) { if (typeof time === 'number') return time * 1000; if (typeof time === 'string') return DateNow(); // Fallback for string timestamps if (typeof time === 'object' && time !== null) return +time; return time; } // Private symbols const kRoot = Symbol('kRoot'); const kReadonly = Symbol('kReadonly'); const kStatWatchers = Symbol('kStatWatchers'); // Entry types const TYPE_FILE = 0; const TYPE_DIR = 1; const TYPE_SYMLINK = 2; // Maximum symlink resolution depth const kMaxSymlinkDepth = 40; /** * Internal entry representation for MemoryProvider. */ class MemoryEntry { constructor(type, options = kEmptyObject) { this.type = type; this.mode = options.mode ?? (type === TYPE_DIR ? 0o755 : 0o644); this.content = null; // For files - static Buffer content this.contentProvider = null; // For files - dynamic content function this.target = null; // For symlinks this.children = null; // For directories this.populate = null; // For directories - lazy population callback this.populated = true; // For directories - has populate been called? this.nlink = 1; this.uid = 0; this.gid = 0; const now = DateNow(); this.atime = now; this.mtime = now; this.ctime = now; this.birthtime = now; } /** * Gets the file content synchronously. * Throws if the content provider returns a Promise. * @returns {Buffer} The file content */ getContentSync() { if (this.contentProvider !== null) { const result = this.contentProvider(); if (isPromise(result)) { // It's a Promise - can't use sync API throw new ERR_INVALID_STATE('cannot use sync API with async content provider'); } return typeof result === 'string' ? Buffer.from(result) : result; } return this.content; } /** * Gets the file content asynchronously. * @returns {Promise<Buffer>} The file content */ async getContentAsync() { if (this.contentProvider !== null) { const result = await this.contentProvider(); return typeof result === 'string' ? Buffer.from(result) : result; } return this.content; } /** * Returns true if this file has a dynamic content provider. * @returns {boolean} */ isDynamic() { return this.contentProvider !== null; } isFile() { return this.type === TYPE_FILE; } isDirectory() { return this.type === TYPE_DIR; } isSymbolicLink() { return this.type === TYPE_SYMLINK; } } /** * In-memory filesystem provider. * Supports full read/write operations. */ class MemoryProvider extends VirtualProvider { constructor() { super(); // Root directory this[kRoot] = new MemoryEntry(TYPE_DIR); this[kRoot].children = new SafeMap(); this[kReadonly] = false; // Map of path -> VFSStatWatcher for watchFile this[kStatWatchers] = new SafeMap(); } get readonly() { return this[kReadonly]; } get supportsWatch() { return true; } /** * Sets the provider to read-only mode. * Once set to read-only, the provider cannot be changed back to writable. * This is useful for finalizing a VFS after initial population. */ setReadOnly() { this[kReadonly] = true; } get supportsSymlinks() { return true; } /** * Normalizes a path to use forward slashes, removes trailing slash, * and resolves . and .. components. * @param {string} path The path to normalize * @returns {string} Normalized path */ #normalizePath(path) { // Convert backslashes to forward slashes let normalized = StringPrototypeReplaceAll(path, '\\', '/'); // Ensure absolute path if (normalized[0] !== '/') { normalized = '/' + normalized; } // Use path.posix.normalize to resolve . and .. return pathPosix.normalize(normalized); } /** * Splits a path into segments. * @param {string} path Normalized path * @returns {string[]} Path segments */ #splitPath(path) { if (path === '/') { return []; } return path.slice(1).split('/'); } /** * Resolves a symlink target to an absolute path. * @param {string} symlinkPath The path of the symlink * @param {string} target The symlink target * @returns {string} Resolved absolute path */ #resolveSymlinkTarget(symlinkPath, target) { if (target.startsWith('/')) { return this.#normalizePath(target); } // Relative target: resolve against symlink's parent directory const parentPath = pathPosix.dirname(symlinkPath); return this.#normalizePath(pathPosix.join(parentPath, target)); } /** * Looks up an entry by path, optionally following symlinks. * @param {string} path The path to look up * @param {boolean} followSymlinks Whether to follow symlinks * @param {number} depth Current symlink resolution depth * @returns {{ entry: MemoryEntry|null, resolvedPath: string|null, eloop?: boolean }} */ #lookupEntry(path, followSymlinks = true, depth = 0) { const normalized = this.#normalizePath(path); if (normalized === '/') { return { entry: this[kRoot], resolvedPath: '/' }; } const segments = this.#splitPath(normalized); let current = this[kRoot]; let currentPath = '/'; for (let i = 0; i < segments.length; i++) { const segment = segments[i]; // Always follow symlinks for intermediate path components if (current.isSymbolicLink()) { if (depth >= kMaxSymlinkDepth) { return { entry: null, resolvedPath: null, eloop: true }; } const targetPath = this.#resolveSymlinkTarget(currentPath, current.target); const result = this.#lookupEntry(targetPath, true, depth + 1); if (result.eloop) { return result; } if (!result.entry) { return { entry: null, resolvedPath: null }; } current = result.entry; currentPath = result.resolvedPath; } if (!current.isDirectory()) { return { entry: null, resolvedPath: null }; } // Ensure directory is populated before accessing children this.#ensurePopulated(current, currentPath); const entry = current.children.get(segment); if (!entry) { return { entry: null, resolvedPath: null }; } currentPath = pathPosix.join(currentPath, segment); current = entry; } // Follow symlink at the end if requested if (current.isSymbolicLink() && followSymlinks) { if (depth >= kMaxSymlinkDepth) { return { entry: null, resolvedPath: null, eloop: true }; } const targetPath = this.#resolveSymlinkTarget(currentPath, current.target); return this.#lookupEntry(targetPath, true, depth + 1); } return { entry: current, resolvedPath: currentPath }; } /** * Gets an entry by path, throwing if not found. * @param {string} path The path * @param {string} syscall The syscall name for error * @param {boolean} followSymlinks Whether to follow symlinks * @returns {MemoryEntry} */ #getEntry(path, syscall, followSymlinks = true) { const result = this.#lookupEntry(path, followSymlinks); if (result.eloop) { throw createELOOP(syscall, path); } if (!result.entry) { throw createENOENT(syscall, path); } return result.entry; } /** * Ensures parent directories exist, optionally creating them. * @param {string} path The full path * @param {boolean} create Whether to create missing directories * @param {string} syscall The syscall name for errors * @returns {MemoryEntry} The parent directory entry */ #ensureParent(path, create, syscall) { if (path === '/') { return this[kRoot]; } const parentPath = pathPosix.dirname(path); const segments = this.#splitPath(parentPath); let current = this[kRoot]; for (let i = 0; i < segments.length; i++) { const segment = segments[i]; const currentPath = pathPosix.join('/', ...segments.slice(0, i)); // Follow symlinks in parent path if (current.isSymbolicLink()) { const targetPath = this.#resolveSymlinkTarget(currentPath, current.target); const result = this.#lookupEntry(targetPath, true, 0); if (!result.entry) { throw createENOENT(syscall, path); } current = result.entry; } if (!current.isDirectory()) { throw createENOTDIR(syscall, path); } // Ensure directory is populated before accessing children this.#ensurePopulated(current, currentPath); let entry = current.children.get(segment); if (!entry) { if (create) { entry = new MemoryEntry(TYPE_DIR); entry.children = new SafeMap(); current.children.set(segment, entry); } else { throw createENOENT(syscall, path); } } current = entry; } // Follow symlinks on the final parent entry if (current.isSymbolicLink()) { const targetPath = this.#resolveSymlinkTarget(parentPath, current.target); const result = this.#lookupEntry(targetPath, true, 0); if (!result.entry) { throw createENOENT(syscall, path); } current = result.entry; } if (!current.isDirectory()) { throw createENOTDIR(syscall, path); } // Ensure final directory is populated this.#ensurePopulated(current, parentPath); return current; } /** * Creates stats for an entry. * @param {MemoryEntry} entry The entry * @param {number} [size] Override size for files * @returns {Stats} */ #createStats(entry, size, bigint) { const options = { mode: entry.mode, nlink: entry.nlink, uid: entry.uid, gid: entry.gid, atimeMs: entry.atime, mtimeMs: entry.mtime, ctimeMs: entry.ctime, birthtimeMs: entry.birthtime, bigint, }; if (entry.isFile()) { let fileSize = size; if (fileSize === undefined) { fileSize = entry.isDynamic() ? entry.getContentSync().length : entry.content.length; } return createFileStats(fileSize, options); } else if (entry.isDirectory()) { return createDirectoryStats(options); } else if (entry.isSymbolicLink()) { return createSymlinkStats(entry.target.length, options); } throw new ERR_INVALID_STATE('Unknown entry type'); } /** * Ensures a directory is populated by calling its populate callback if needed. * @param {MemoryEntry} entry The directory entry * @param {string} path The directory path (for error messages and scoped VFS) */ #ensurePopulated(entry, path) { if (entry.isDirectory() && !entry.populated && entry.populate) { // Create a scoped VFS for the populate callback const scopedVfs = { addFile: (name, content, opts) => { const fileEntry = new MemoryEntry(TYPE_FILE, opts); if (typeof content === 'function') { fileEntry.content = Buffer.alloc(0); fileEntry.contentProvider = content; } else { fileEntry.content = typeof content === 'string' ? Buffer.from(content) : content; } entry.children.set(name, fileEntry); }, addDirectory: (name, populate, opts) => { const dirEntry = new MemoryEntry(TYPE_DIR, opts); dirEntry.children = new SafeMap(); if (typeof populate === 'function') { dirEntry.populate = populate; dirEntry.populated = false; } entry.children.set(name, dirEntry); }, addSymlink: (name, target, opts) => { const symlinkEntry = new MemoryEntry(TYPE_SYMLINK, opts); symlinkEntry.target = target; entry.children.set(name, symlinkEntry); }, }; entry.populate(scopedVfs); entry.populated = true; } } openSync(path, flags, mode) { const normalized = this.#normalizePath(path); // 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) { throw createEROFS('open', path); } let entry; try { entry = this.#getEntry(normalized, 'open'); // Exclusive flag: file must not exist if (isExclusive) { throw createEEXIST('open', path); } } catch (err) { if (err.code !== 'ENOENT' || !isCreate) throw err; // Create the file const parent = this.#ensureParent(normalized, false, 'open'); const name = pathPosix.basename(normalized); entry = new MemoryEntry(TYPE_FILE, { mode }); entry.content = Buffer.alloc(0); parent.children.set(name, entry); const now = DateNow(); parent.mtime = now; parent.ctime = now; } if (entry.isDirectory()) { throw createEISDIR('open', path); } if (entry.isSymbolicLink()) { // Should have been resolved already, but just in case throw createEINVAL('open', path); } const getStats = (size) => this.#createStats(entry, size); return new MemoryFileHandle(normalized, flags, mode ?? entry.mode, entry.content, entry, getStats); } async open(path, flags, mode) { return this.openSync(path, flags, mode); } statSync(path, options) { const entry = this.#getEntry(path, 'stat', true); return this.#createStats(entry, undefined, options?.bigint); } async stat(path, options) { return this.statSync(path, options); } lstatSync(path, options) { const entry = this.#getEntry(path, 'lstat', false); return this.#createStats(entry, undefined, options?.bigint); } async lstat(path, options) { return this.lstatSync(path, options); } readdirSync(path, options) { const entry = this.#getEntry(path, 'scandir', true); if (!entry.isDirectory()) { throw createENOTDIR('scandir', path); } // Ensure directory is populated (for lazy population) this.#ensurePopulated(entry, path); const normalized = this.#normalizePath(path); const withFileTypes = options?.withFileTypes === true; const recursive = options?.recursive === true; if (recursive) { return this.#readdirRecursive(entry, normalized, withFileTypes); } if (withFileTypes) { const dirents = []; for (const { 0: name, 1: childEntry } of entry.children) { let type; if (childEntry.isSymbolicLink()) { type = UV_DIRENT_LINK; } else if (childEntry.isDirectory()) { type = UV_DIRENT_DIR; } else { type = UV_DIRENT_FILE; } ArrayPrototypePush(dirents, new Dirent(name, type, normalized)); } return dirents; } return ArrayFrom(entry.children.keys()); } /** * Recursively reads directory contents. * @param {MemoryEntry} dirEntry The directory entry * @param {string} dirPath The normalized directory path * @param {boolean} withFileTypes Whether to return Dirent objects * @returns {string[]|Dirent[]} */ #readdirRecursive(dirEntry, dirPath, withFileTypes) { const results = []; // Directories on the current traversal path. A directory reached again // through a symlink cycle is not descended into (but is still listed). const active = new SafeSet(); // Traverse depth-first with an explicit stack instead of recursion, so a // deeply nested tree cannot exhaust the call stack. Each frame is a // directory being walked together with a snapshot of its children and the // index of the next child to visit. const enter = (entry, currentPath, relativePath) => { this.#ensurePopulated(entry, currentPath); active.add(entry); ArrayPrototypePush(stack, { entry, currentPath, relativePath, children: ArrayFrom(entry.children), index: 0, }); }; const stack = []; enter(dirEntry, dirPath, ''); while (stack.length > 0) { const frame = stack[stack.length - 1]; if (frame.index >= frame.children.length) { active.delete(frame.entry); ArrayPrototypePop(stack); continue; } const { 0: name, 1: childEntry } = frame.children[frame.index++]; const childRelative = frame.relativePath ? frame.relativePath + '/' + name : name; if (withFileTypes) { let type; if (childEntry.isSymbolicLink()) { type = UV_DIRENT_LINK; } else if (childEntry.isDirectory()) { type = UV_DIRENT_DIR; } else { type = UV_DIRENT_FILE; } ArrayPrototypePush(results, new Dirent(childRelative, type, dirPath)); } else { ArrayPrototypePush(results, childRelative); } // Follow symlinks to directories for recursive traversal, skipping any // directory already on the active path to avoid symlink cycles. let resolvedChild = childEntry; if (childEntry.isSymbolicLink()) { const targetPath = this.#resolveSymlinkTarget( pathPosix.join(frame.currentPath, name), childEntry.target, ); const result = this.#lookupEntry(targetPath, true, 0); if (result.entry) { resolvedChild = result.entry; } } if (resolvedChild.isDirectory() && !active.has(resolvedChild)) { enter(resolvedChild, pathPosix.join(frame.currentPath, name), childRelative); } } return results; } async readdir(path, options) { return this.readdirSync(path, options); } mkdirSync(path, options) { if (this.readonly) { throw createEROFS('mkdir', path); } const normalized = this.#normalizePath(path); const recursive = options?.recursive === true; // Check if already exists const existing = this.#lookupEntry(normalized, true); if (existing.entry) { if (existing.entry.isDirectory() && recursive) { // Already exists, that's ok for recursive return undefined; } throw createEEXIST('mkdir', path); } if (recursive) { // Create all parent directories const segments = this.#splitPath(normalized); let current = this[kRoot]; let currentPath = '/'; let resolvedCurrentPath = '/'; let firstCreated; for (const segment of segments) { currentPath = pathPosix.join(currentPath, segment); const resolvedPath = pathPosix.join(resolvedCurrentPath, segment); this.#ensurePopulated(current, resolvedCurrentPath); let entry = current.children.get(segment); if (!entry) { entry = new MemoryEntry(TYPE_DIR, { mode: options?.mode }); entry.children = new SafeMap(); current.children.set(segment, entry); if (firstCreated === undefined) { firstCreated = currentPath; } resolvedCurrentPath = resolvedPath; } else if (entry.isSymbolicLink()) { const targetPath = this.#resolveSymlinkTarget(resolvedPath, entry.target); const result = this.#lookupEntry(targetPath, true, 0); if (result.eloop) { throw createELOOP('mkdir', path); } if (!result.entry) { throw createENOENT('mkdir', path); } entry = result.entry; resolvedCurrentPath = result.resolvedPath; } else { resolvedCurrentPath = resolvedPath; } if (!entry.isDirectory()) { throw createENOTDIR('mkdir', path); } current = entry; } return firstCreated; } const parent = this.#ensureParent(normalized, false, 'mkdir'); const name = pathPosix.basename(normalized); const entry = new MemoryEntry(TYPE_DIR, { mode: options?.mode }); entry.children = new SafeMap(); parent.children.set(name, entry); const now = DateNow(); parent.mtime = now; parent.ctime = now; return undefined; } async mkdir(path, options) { return this.mkdirSync(path, options); } rmdirSync(path) { if (this.readonly) { throw createEROFS('rmdir', path); } const normalized = this.#normalizePath(path); const entry = this.#getEntry(normalized, 'rmdir', false); if (!entry.isDirectory()) { throw createENOTDIR('rmdir', path); } if (entry.children.size > 0) { throw createENOTEMPTY('rmdir', path); } const parent = this.#ensureParent(normalized, false, 'rmdir'); const name = pathPosix.basename(normalized); parent.children.delete(name); const now = DateNow(); parent.mtime = now; parent.ctime = now; } async rmdir(path) { this.rmdirSync(path); } unlinkSync(path) { if (this.readonly) { throw createEROFS('unlink', path); } const normalized = this.#normalizePath(path); const entry = this.#getEntry(normalized, 'unlink', false); if (entry.isDirectory()) { throw createEISDIR('unlink', path); } const parent = this.#ensureParent(normalized, false, 'unlink'); const name = pathPosix.basename(normalized); parent.children.delete(name); entry.nlink--; const now = DateNow(); parent.mtime = now; parent.ctime = now; } async unlink(path) { this.unlinkSync(path); } renameSync(oldPath, newPath) { if (this.readonly) { throw createEROFS('rename', oldPath); } const normalizedOld = this.#normalizePath(oldPath); const normalizedNew = this.#normalizePath(newPath); // Get the entry (without following symlinks for the entry itself) const entry = this.#getEntry(normalizedOld, 'rename', false); if (entry.isDirectory() && StringPrototypeStartsWith(normalizedNew, `${normalizedOld}/`)) { throw createEINVAL('rename', oldPath); } // Validate destination parent exists (do not auto-create) const newParent = this.#ensureParent(normalizedNew, false, 'rename'); const newName = pathPosix.basename(normalizedNew); // Check if destination exists const existingDest = newParent.children.get(newName); if (existingDest) { // Cannot overwrite a directory with a non-directory if (existingDest.isDirectory() && !entry.isDirectory()) { throw createEISDIR('rename', newPath); } // Cannot overwrite a non-directory with a directory if (!existingDest.isDirectory() && entry.isDirectory()) { throw createENOTDIR('rename', newPath); } } // Remove from old location (after destination validation) const oldParent = this.#ensureParent(normalizedOld, false, 'rename'); const oldName = pathPosix.basename(normalizedOld); oldParent.children.delete(oldName); // Add to new location newParent.children.set(newName, entry); const now = DateNow(); oldParent.mtime = now; oldParent.ctime = now; if (newParent !== oldParent) { newParent.mtime = now; newParent.ctime = now; } } async rename(oldPath, newPath) { this.renameSync(oldPath, newPath); } linkSync(existingPath, newPath) { if (this.readonly) { throw createEROFS('link', newPath); } const normalizedExisting = this.#normalizePath(existingPath); const normalizedNew = this.#normalizePath(newPath); const entry = this.#getEntry(normalizedExisting, 'link', true); if (!entry.isFile()) { // Hard links to directories are not supported throw createEINVAL('link', existingPath); } // Check if new path already exists const existing = this.#lookupEntry(normalizedNew, false); if (existing.entry) { throw createEEXIST('link', newPath); } const parent = this.#ensureParent(normalizedNew, false, 'link'); const name = pathPosix.basename(normalizedNew); // Hard link: same entry object referenced by both names parent.children.set(name, entry); entry.nlink++; const now = DateNow(); parent.mtime = now; parent.ctime = now; } async link(existingPath, newPath) { this.linkSync(existingPath, newPath); } readlinkSync(path, options) { const normalized = this.#normalizePath(path); const entry = this.#getEntry(normalized, 'readlink', false); if (!entry.isSymbolicLink()) { throw createEINVAL('readlink', path); } return entry.target; } async readlink(path, options) { return this.readlinkSync(path, options); } symlinkSync(target, path, type) { if (this.readonly) { throw createEROFS('symlink', path); } const normalized = this.#normalizePath(path); // Check if already exists const existing = this.#lookupEntry(normalized, false); if (existing.entry) { throw createEEXIST('symlink', path); } const parent = this.#ensureParent(normalized, false, 'symlink'); const name = pathPosix.basename(normalized); const entry = new MemoryEntry(TYPE_SYMLINK); entry.target = target; parent.children.set(name, entry); const now = DateNow(); parent.mtime = now; parent.ctime = now; } async symlink(target, path, type) { this.symlinkSync(target, path, type); } realpathSync(path, options) { const result = this.#lookupEntry(path, true, 0); if (result.eloop) { throw createELOOP('realpath', path); } if (!result.entry) { throw createENOENT('realpath', path); } return result.resolvedPath; } async realpath(path, options) { return this.realpathSync(path, options); } // === METADATA OPERATIONS === chmodSync(path, mode) { const entry = this.#getEntry(path, 'chmod', true); // Preserve file type bits, update permission bits entry.mode = (entry.mode & ~0o7777) | (mode & 0o7777); entry.ctime = DateNow(); } lchmodSync(path, mode) { const entry = this.#getEntry(path, 'chmod', false); // Preserve file type bits, update permission bits entry.mode = (entry.mode & ~0o7777) | (mode & 0o7777); entry.ctime = DateNow(); } chownSync(path, uid, gid) { const entry = this.#getEntry(path, 'chown', true); if (uid >= 0) entry.uid = uid; if (gid >= 0) entry.gid = gid; entry.ctime = DateNow(); } lchownSync(path, uid, gid) { const entry = this.#getEntry(path, 'chown', false); if (uid >= 0) entry.uid = uid; if (gid >= 0) entry.gid = gid; entry.ctime = DateNow(); } utimesSync(path, atime, mtime) { const entry = this.#getEntry(path, 'utime', true); entry.atime = toMs(atime); entry.mtime = toMs(mtime); entry.ctime = DateNow(); } lutimesSync(path, atime, mtime) { const entry = this.#getEntry(path, 'utime', false); entry.atime = toMs(atime); entry.mtime = toMs(mtime); entry.ctime = DateNow(); } // === WATCH OPERATIONS === /** * Watches a file or directory for changes. * @param {string} path The path to watch * @param {object} [options] Watch options * @returns {VFSWatcher} */ watch(path, options) { const normalized = this.#normalizePath(path); return new VFSWatcher(this, normalized, options); } /** * Watches a file or directory for changes (async iterable version). * Used by fs.promises.watch(). * @param {string} path The path to watch * @param {object} [options] Watch options * @returns {VFSWatchAsyncIterable} */ watchAsync(path, options) { const normalized = this.#normalizePath(path); return new VFSWatchAsyncIterable(this, normalized, options); } /** * Watches a file for changes using stat polling. * @param {string} path The path to watch * @param {object} [options] Watch options * @param {Function} [listener] Change listener * @returns {VFSStatWatcher} */ watchFile(path, options, listener) { const normalized = this.#normalizePath(path); // Reuse existing watcher for the same path let watcher = this[kStatWatchers].get(normalized); if (!watcher) { watcher = new VFSStatWatcher(this, normalized, options); this[kStatWatchers].set(normalized, watcher); } if (listener) { watcher.addListener('change', listener); } return watcher; } /** * Stops watching a file for changes. * @param {string} path The path to stop watching * @param {Function} [listener] Optional listener to remove */ unwatchFile(path, listener) { const normalized = this.#normalizePath(path); const watcher = this[kStatWatchers].get(normalized); if (!watcher) { return; } if (listener) { watcher.removeListener('change', listener); } else { // Remove all listeners watcher.removeAllListeners('change'); } // If no more listeners, stop and remove the watcher if (watcher.hasNoListeners()) { watcher.stop(); this[kStatWatchers].delete(normalized); } } } module.exports = { MemoryProvider, };