Skip to content

Commit f9826ef

Browse files
committed
vfs: add symbolic link support
Add comprehensive symbolic link support to the Virtual File System: - Add VirtualSymlink class for symlink entries - Add createSymlinkStats() for symlink stat objects - Add addSymlink(path, target) method to create symlinks - Implement symlink resolution during path traversal - statSync() follows symlinks, lstatSync() does not - Add readlinkSync()/readlink() to read symlink targets - realpathSync()/realpath() resolve symlink chains - readdirSync() returns UV_DIRENT_LINK for symlinks - Support relative and absolute symlink targets - Handle symlink loops with ELOOP error (max depth: 40) - Support symlinks in dynamic directories via scoped VFS
1 parent a3cb9ac commit f9826ef

File tree

5 files changed

+740
-42
lines changed

5 files changed

+740
-42
lines changed

lib/internal/vfs/entries.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const {
1313
},
1414
} = require('internal/errors');
1515
const { join } = require('path');
16-
const { createFileStats, createDirectoryStats } = require('internal/vfs/stats');
16+
const { createFileStats, createDirectoryStats, createSymlinkStats } = require('internal/vfs/stats');
1717

1818
// Symbols for private properties
1919
const kContent = Symbol('kContent');
@@ -23,6 +23,7 @@ const kPopulated = Symbol('kPopulated');
2323
const kEntries = Symbol('kEntries');
2424
const kStats = Symbol('kStats');
2525
const kPath = Symbol('kPath');
26+
const kTarget = Symbol('kTarget');
2627

2728
/**
2829
* Base class for virtual file system entries.
@@ -67,6 +68,14 @@ class VirtualEntry {
6768
isDirectory() {
6869
return false;
6970
}
71+
72+
/**
73+
* Returns true if this entry is a symbolic link.
74+
* @returns {boolean}
75+
*/
76+
isSymbolicLink() {
77+
return false;
78+
}
7079
}
7180

7281
/**
@@ -265,6 +274,38 @@ class VirtualDirectory extends VirtualEntry {
265274
}
266275
}
267276

277+
/**
278+
* Represents a virtual symbolic link.
279+
*/
280+
class VirtualSymlink extends VirtualEntry {
281+
/**
282+
* @param {string} path The absolute path of this symlink
283+
* @param {string} target The symlink target (can be relative or absolute)
284+
* @param {object} [options] Optional configuration
285+
* @param {number} [options.mode] Symlink mode (default: 0o777)
286+
*/
287+
constructor(path, target, options = {}) {
288+
super(path);
289+
this[kTarget] = target;
290+
this[kStats] = createSymlinkStats(target.length, options);
291+
}
292+
293+
/**
294+
* @returns {boolean}
295+
*/
296+
isSymbolicLink() {
297+
return true;
298+
}
299+
300+
/**
301+
* Gets the symlink target path.
302+
* @returns {string}
303+
*/
304+
get target() {
305+
return this[kTarget];
306+
}
307+
}
308+
268309
/**
269310
* Creates a scoped VFS interface for dynamic directory populate callbacks.
270311
* @param {VirtualDirectory} directory The parent directory
@@ -284,13 +325,19 @@ function createScopedVFS(directory, addEntry) {
284325
const dir = new VirtualDirectory(dirPath, populate, options);
285326
addEntry(name, dir);
286327
},
328+
addSymlink(name, target, options) {
329+
const linkPath = join(directory.path, name);
330+
const symlink = new VirtualSymlink(linkPath, target, options);
331+
addEntry(name, symlink);
332+
},
287333
};
288334
}
289335

290336
module.exports = {
291337
VirtualEntry,
292338
VirtualFile,
293339
VirtualDirectory,
340+
VirtualSymlink,
294341
createScopedVFS,
295342
kContent,
296343
kContentProvider,
@@ -299,4 +346,5 @@ module.exports = {
299346
kEntries,
300347
kStats,
301348
kPath,
349+
kTarget,
302350
};

lib/internal/vfs/errors.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const {
1616
UV_EEXIST,
1717
UV_EROFS,
1818
UV_EINVAL,
19+
UV_ELOOP,
1920
} = internalBinding('uv');
2021

2122
/**
@@ -128,6 +129,22 @@ function createEINVAL(syscall, path) {
128129
return err;
129130
}
130131

132+
/**
133+
* Creates an ELOOP error for too many symbolic links.
134+
* @param {string} syscall The system call name
135+
* @param {string} path The path
136+
* @returns {Error}
137+
*/
138+
function createELOOP(syscall, path) {
139+
const err = new UVException({
140+
errno: UV_ELOOP,
141+
syscall,
142+
path,
143+
});
144+
ErrorCaptureStackTrace(err, createELOOP);
145+
return err;
146+
}
147+
131148
module.exports = {
132149
createENOENT,
133150
createENOTDIR,
@@ -136,4 +153,5 @@ module.exports = {
136153
createEEXIST,
137154
createEROFS,
138155
createEINVAL,
156+
createELOOP,
139157
};

lib/internal/vfs/stats.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const {
1111
fs: {
1212
S_IFDIR,
1313
S_IFREG,
14+
S_IFLNK,
1415
},
1516
} = internalBinding('constants');
1617

@@ -136,8 +137,60 @@ function createDirectoryStats(options = {}) {
136137
return getStatsFromBinding(statsArray);
137138
}
138139

140+
/**
141+
* Creates a Stats object for a virtual symbolic link.
142+
* @param {number} size The symlink size (length of target path)
143+
* @param {object} [options] Optional stat properties
144+
* @param {number} [options.mode] Symlink mode (default: 0o777)
145+
* @param {number} [options.uid] User ID (default: process.getuid() or 0)
146+
* @param {number} [options.gid] Group ID (default: process.getgid() or 0)
147+
* @param {number} [options.atimeMs] Access time in ms
148+
* @param {number} [options.mtimeMs] Modification time in ms
149+
* @param {number} [options.ctimeMs] Change time in ms
150+
* @param {number} [options.birthtimeMs] Birth time in ms
151+
* @returns {Stats}
152+
*/
153+
function createSymlinkStats(size, options = {}) {
154+
const now = DateNow();
155+
const mode = (options.mode ?? 0o777) | S_IFLNK;
156+
const uid = options.uid ?? (process.getuid?.() ?? 0);
157+
const gid = options.gid ?? (process.getgid?.() ?? 0);
158+
const atimeMs = options.atimeMs ?? now;
159+
const mtimeMs = options.mtimeMs ?? now;
160+
const ctimeMs = options.ctimeMs ?? now;
161+
const birthtimeMs = options.birthtimeMs ?? now;
162+
const blocks = MathCeil(size / 512);
163+
164+
const atime = msToTimeSpec(atimeMs);
165+
const mtime = msToTimeSpec(mtimeMs);
166+
const ctime = msToTimeSpec(ctimeMs);
167+
const birthtime = msToTimeSpec(birthtimeMs);
168+
169+
statsArray[0] = 0; // dev
170+
statsArray[1] = mode; // mode
171+
statsArray[2] = 1; // nlink
172+
statsArray[3] = uid; // uid
173+
statsArray[4] = gid; // gid
174+
statsArray[5] = 0; // rdev
175+
statsArray[6] = kDefaultBlockSize; // blksize
176+
statsArray[7] = 0; // ino
177+
statsArray[8] = size; // size
178+
statsArray[9] = blocks; // blocks
179+
statsArray[10] = atime.sec; // atime_sec
180+
statsArray[11] = atime.nsec; // atime_nsec
181+
statsArray[12] = mtime.sec; // mtime_sec
182+
statsArray[13] = mtime.nsec; // mtime_nsec
183+
statsArray[14] = ctime.sec; // ctime_sec
184+
statsArray[15] = ctime.nsec; // ctime_nsec
185+
statsArray[16] = birthtime.sec; // birthtime_sec
186+
statsArray[17] = birthtime.nsec; // birthtime_nsec
187+
188+
return getStatsFromBinding(statsArray);
189+
}
190+
139191
module.exports = {
140192
createFileStats,
141193
createDirectoryStats,
194+
createSymlinkStats,
142195
kDefaultBlockSize,
143196
};

0 commit comments

Comments
 (0)