Repository: streamich/memfs
Pull request #1066 (2024-10-02 14:27:20)

feat: implement lutimes
Let's try this again...

Continuation of https://github.com/streamich/memfs/pull/1065

BadIdeaException: "Some checks were not successful"

This is a pre-existing test that failed due to timing out. This is not a test that I wrote, nor does it cover code that I wrote/changed. I think this is a fluke that was already present and probably only manifests sporadically.

Patch:
diff --git a/src/node/lists/fsCallbackApiList.ts b/src/node/lists/fsCallbackApiList.ts
index 9d3773ac..d64a8c6f 100644
--- a/src/node/lists/fsCallbackApiList.ts
+++ b/src/node/lists/fsCallbackApiList.ts
@@ -39,6 +39,7 @@ export const fsCallbackApiList: Array<keyof FsCallbackApi> = [
   'unlink',
   'unwatchFile',
   'utimes',
+  'lutimes',
   'watch',
   'watchFile',
   'write',
diff --git a/src/node/lists/fsSynchronousApiList.ts b/src/node/lists/fsSynchronousApiList.ts
index a02489bb..a5e86ddb 100644
--- a/src/node/lists/fsSynchronousApiList.ts
+++ b/src/node/lists/fsSynchronousApiList.ts
@@ -36,11 +36,11 @@ export const fsSynchronousApiList: Array<keyof FsSynchronousApi> = [
   'truncateSync',
   'unlinkSync',
   'utimesSync',
+  'lutimesSync',
   'writeFileSync',
   'writeSync',
   'writevSync',
 
   // 'cpSync',
-  // 'lutimesSync',
   // 'statfsSync',
 ];
diff --git a/src/volume.ts b/src/volume.ts
index b79fa1d1..1baaa044 100644
--- a/src/volume.ts
+++ b/src/volume.ts
@@ -1742,19 +1742,37 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
     this.wrapAsync(this.futimesBase, [fd, toUnixTimestamp(atime), toUnixTimestamp(mtime)], callback);
   }
 
-  private utimesBase(filename: string, atime: number, mtime: number) {
-    const link = this.getResolvedLinkOrThrow(filename, 'utimes');
+  private utimesBase(filename: string, atime: number, mtime: number, followSymlinks: boolean = true) {
+    const link = followSymlinks
+      ? this.getResolvedLinkOrThrow(filename, 'utimes')
+      : this.getLinkOrThrow(filename, 'lutimes');
     const node = link.getNode();
     node.atime = new Date(atime * 1000);
     node.mtime = new Date(mtime * 1000);
   }
 
   utimesSync(path: PathLike, atime: TTime, mtime: TTime) {
-    this.utimesBase(pathToFilename(path), toUnixTimestamp(atime), toUnixTimestamp(mtime));
+    this.utimesBase(pathToFilename(path), toUnixTimestamp(atime), toUnixTimestamp(mtime), true);
   }
 
   utimes(path: PathLike, atime: TTime, mtime: TTime, callback: TCallback<void>) {
-    this.wrapAsync(this.utimesBase, [pathToFilename(path), toUnixTimestamp(atime), toUnixTimestamp(mtime)], callback);
+    this.wrapAsync(
+      this.utimesBase,
+      [pathToFilename(path), toUnixTimestamp(atime), toUnixTimestamp(mtime), true],
+      callback,
+    );
+  }
+
+  lutimesSync(path: PathLike, atime: TTime, mtime: TTime): void {
+    this.utimesBase(pathToFilename(path), toUnixTimestamp(atime), toUnixTimestamp(mtime), false);
+  }
+
+  lutimes(path: PathLike, atime: TTime, mtime: TTime, callback: TCallback<void>): void {
+    this.wrapAsync(
+      this.utimesBase,
+      [pathToFilename(path), toUnixTimestamp(atime), toUnixTimestamp(mtime), false],
+      callback,
+    );
   }
 
   private mkdirBase(filename: string, modeNum: number) {
@@ -2130,11 +2148,9 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
   }
 
   public cpSync: FsSynchronousApi['cpSync'] = notImplemented;
-  public lutimesSync: FsSynchronousApi['lutimesSync'] = notImplemented;
   public statfsSync: FsSynchronousApi['statfsSync'] = notImplemented;
 
   public cp: FsCallbackApi['cp'] = notImplemented;
-  public lutimes: FsCallbackApi['lutimes'] = notImplemented;
   public statfs: FsCallbackApi['statfs'] = notImplemented;
   public openAsBlob: FsCallbackApi['openAsBlob'] = notImplemented;
 


Test patch:
diff --git a/src/__tests__/volume/lutimesSync.test.ts b/src/__tests__/volume/lutimesSync.test.ts
new file mode 100644
index 00000000..11735a6f
--- /dev/null
+++ b/src/__tests__/volume/lutimesSync.test.ts
@@ -0,0 +1,66 @@
+import { create } from '../util';
+
+describe('lutimesSync', () => {
+  it('should be able to lutimes symlinks regardless of their permissions', () => {
+    const perms = [
+      0o777, // rwx
+      0o666, // rw
+      0o555, // rx
+      0o444, // r
+      0o333, // wx
+      0o222, // w
+      0o111, // x
+      0o000, // none
+    ];
+    // Check for directories
+    perms.forEach(perm => {
+      const vol = create({ '/target': 'test' });
+      vol.symlinkSync('/target', '/test');
+      expect(() => {
+        vol.lutimesSync('/test', 0, 0);
+      }).not.toThrow();
+    });
+  });
+
+  it('should set atime and mtime on the link itself, not the target', () => {
+    const vol = create({ '/target': 'test' });
+    vol.symlinkSync('/target', '/test');
+    vol.lutimesSync('/test', new Date(1), new Date(2));
+    const linkStats = vol.lstatSync('/test');
+    const targetStats = vol.statSync('/target');
+
+    expect(linkStats.atime).toEqual(new Date(1));
+    expect(linkStats.mtime).toEqual(new Date(2));
+
+    expect(targetStats.atime).not.toEqual(new Date(1));
+    expect(targetStats.mtime).not.toEqual(new Date(2));
+  });
+
+  it("should throw ENOENT when target doesn't exist", () => {
+    const vol = create({ '/target': 'test' });
+    // Don't create symlink this time
+    expect(() => {
+      vol.lutimesSync('/test', 0, 0);
+    }).toThrow(/ENOENT/);
+  });
+
+  it('should throw EACCES when containing directory has insufficient permissions', () => {
+    const vol = create({ '/target': 'test' });
+    vol.mkdirSync('/foo');
+    vol.symlinkSync('/target', '/foo/test');
+    vol.chmodSync('/foo', 0o666); // rw
+    expect(() => {
+      vol.lutimesSync('/foo/test', 0, 0);
+    }).toThrow(/EACCES/);
+  });
+
+  it('should throw EACCES when intermediate directory has insufficient permissions', () => {
+    const vol = create({ '/target': 'test' });
+    vol.mkdirSync('/foo');
+    vol.symlinkSync('/target', '/foo/test');
+    vol.chmodSync('/', 0o666); // rw
+    expect(() => {
+      vol.lutimesSync('/foo/test', 0, 0);
+    }).toThrow(/EACCES/);
+  });
+});