Skip to content

Commit 4345185

Browse files
trivikraduh95
authored andcommitted
vfs: make lchown update symlink metadata
Route mounted lchown operations through VFS lchown handling and add memory provider support that does not follow the final symlink. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5 PR-URL: #64573 Fixes: #64572 Reviewed-By: Aviv Keller <me@aviv.sh> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 3bc0ee0 commit 4345185

6 files changed

Lines changed: 75 additions & 2 deletions

File tree

doc/api/vfs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ signatures as their [`node:fs`][] counterparts:
156156
* `linkSync(existingPath, newPath)`
157157
* `chmodSync(path, mode)`
158158
* `chownSync(path, uid, gid)`
159+
* `lchownSync(path, uid, gid)`
159160
* `utimesSync(path, atime, mtime)`
160161
* `lutimesSync(path, atime, mtime)`
161162
* `mkdtempSync(prefix)`

lib/internal/vfs/file_system.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,11 @@ class VirtualFileSystem {
506506
this[kProvider].chownSync(providerPath, uid, gid);
507507
}
508508

509+
lchownSync(filePath, uid, gid) {
510+
const providerPath = this.#toProviderPath(filePath);
511+
this[kProvider].lchownSync(providerPath, uid, gid);
512+
}
513+
509514
utimesSync(filePath, atime, mtime) {
510515
const providerPath = this.#toProviderPath(filePath);
511516
this[kProvider].utimesSync(providerPath, atime, mtime);
@@ -1234,7 +1239,7 @@ class VirtualFileSystem {
12341239

12351240
async lchown(filePath, uid, gid) {
12361241
const providerPath = toProviderPath(filePath);
1237-
provider.chownSync(providerPath, uid, gid);
1242+
provider.lchownSync(providerPath, uid, gid);
12381243
},
12391244

12401245
async utimes(filePath, atime, mtime) {

lib/internal/vfs/provider.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,18 @@ class VirtualProvider {
235235
throw new ERR_METHOD_NOT_IMPLEMENTED('renameSync');
236236
}
237237

238+
/**
239+
* Changes ownership of a path without following the final symbolic link.
240+
* Providers with symlink support should override this.
241+
* @param {string} path The path
242+
* @param {number} uid The user id
243+
* @param {number} gid The group id
244+
* @returns {void}
245+
*/
246+
lchownSync(path, uid, gid) {
247+
return this.chownSync(path, uid, gid);
248+
}
249+
238250
// === DEFAULT IMPLEMENTATIONS (built on primitives) ===
239251

240252
/**

lib/internal/vfs/providers/memory.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,13 @@ class MemoryProvider extends VirtualProvider {
986986
entry.ctime = DateNow();
987987
}
988988

989+
lchownSync(path, uid, gid) {
990+
const entry = this.#getEntry(path, 'chown', false);
991+
if (uid >= 0) entry.uid = uid;
992+
if (gid >= 0) entry.gid = gid;
993+
entry.ctime = DateNow();
994+
}
995+
989996
utimesSync(path, atime, mtime) {
990997
const entry = this.#getEntry(path, 'utime', true);
991998
entry.atime = toMs(atime);

lib/internal/vfs/setup.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ function createVfsHandlers() {
343343
vfsOpVoid(path, (vfs, n) => vfs.symlinkSync(target, n, type)),
344344
chmodSync: (path, mode) => vfsOpVoid(path, (vfs, n) => vfs.chmodSync(n, mode)),
345345
chownSync: (path, uid, gid) => vfsOpVoid(path, (vfs, n) => vfs.chownSync(n, uid, gid)),
346-
lchownSync: (path, uid, gid) => vfsOpVoid(path, (vfs, n) => vfs.chownSync(n, uid, gid)),
346+
lchownSync: (path, uid, gid) => vfsOpVoid(path, (vfs, n) => vfs.lchownSync(n, uid, gid)),
347347
utimesSync: (path, atime, mtime) =>
348348
vfsOpVoid(path, (vfs, n) => vfs.utimesSync(n, atime, mtime)),
349349
lutimesSync: (path, atime, mtime) =>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Flags: --experimental-vfs
2+
'use strict';
3+
4+
const common = require('../common');
5+
const assert = require('assert');
6+
const fs = require('fs');
7+
const fsp = require('fs/promises');
8+
const path = require('path');
9+
const vfs = require('node:vfs');
10+
11+
const mountPoint = path.resolve('/tmp/vfs-lchown-' + process.pid);
12+
const myVfs = vfs.create();
13+
myVfs.writeFileSync('/sync-target.txt', 'target');
14+
myVfs.symlinkSync('/sync-target.txt', '/sync-link.txt');
15+
myVfs.writeFileSync('/async-target.txt', 'target');
16+
myVfs.symlinkSync('/async-target.txt', '/async-link.txt');
17+
myVfs.writeFileSync('/promise-target.txt', 'target');
18+
myVfs.symlinkSync('/promise-target.txt', '/promise-link.txt');
19+
myVfs.mount(mountPoint);
20+
21+
function assertOwnership(filePath, uid, gid) {
22+
const stats = fs.lstatSync(path.join(mountPoint, filePath));
23+
assert.strictEqual(stats.uid, uid);
24+
assert.strictEqual(stats.gid, gid);
25+
}
26+
27+
(async () => {
28+
try {
29+
fs.lchownSync(path.join(mountPoint, 'sync-link.txt'), 123, 456);
30+
assertOwnership('/sync-target.txt', 0, 0);
31+
assertOwnership('/sync-link.txt', 123, 456);
32+
33+
await new Promise((resolve, reject) => {
34+
fs.lchown(path.join(mountPoint, 'async-link.txt'), 234, 567, (err) => {
35+
if (err) reject(err);
36+
else resolve();
37+
});
38+
});
39+
assertOwnership('/async-target.txt', 0, 0);
40+
assertOwnership('/async-link.txt', 234, 567);
41+
42+
await fsp.lchown(path.join(mountPoint, 'promise-link.txt'), 345, 678);
43+
assertOwnership('/promise-target.txt', 0, 0);
44+
assertOwnership('/promise-link.txt', 345, 678);
45+
} finally {
46+
myVfs.unmount();
47+
}
48+
})().then(common.mustCall());

0 commit comments

Comments
 (0)