From a1772b25fbd9ca5d29a5bce9c848206b42ff2652 Mon Sep 17 00:00:00 2001 From: BadIdeaException Date: Mon, 7 Oct 2024 20:07:42 +0200 Subject: [PATCH] feat: implement lutimes (#1066) * feat: implement lutimes * fix: lutimes/lutimesSync not exported on fs --- src/__tests__/volume/lutimesSync.test.ts | 66 ++++++++++++++++++++++++ src/node/lists/fsCallbackApiList.ts | 1 + src/node/lists/fsSynchronousApiList.ts | 2 +- src/volume.ts | 28 +++++++--- 4 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 src/__tests__/volume/lutimesSync.test.ts diff --git a/src/__tests__/volume/lutimesSync.test.ts b/src/__tests__/volume/lutimesSync.test.ts new file mode 100644 index 000000000..11735a6fe --- /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/); + }); +}); diff --git a/src/node/lists/fsCallbackApiList.ts b/src/node/lists/fsCallbackApiList.ts index 9d3773ac7..d64a8c6f1 100644 --- a/src/node/lists/fsCallbackApiList.ts +++ b/src/node/lists/fsCallbackApiList.ts @@ -39,6 +39,7 @@ export const fsCallbackApiList: Array = [ 'unlink', 'unwatchFile', 'utimes', + 'lutimes', 'watch', 'watchFile', 'write', diff --git a/src/node/lists/fsSynchronousApiList.ts b/src/node/lists/fsSynchronousApiList.ts index a02489bb4..a5e86ddb8 100644 --- a/src/node/lists/fsSynchronousApiList.ts +++ b/src/node/lists/fsSynchronousApiList.ts @@ -36,11 +36,11 @@ export const fsSynchronousApiList: Array = [ 'truncateSync', 'unlinkSync', 'utimesSync', + 'lutimesSync', 'writeFileSync', 'writeSync', 'writevSync', // 'cpSync', - // 'lutimesSync', // 'statfsSync', ]; diff --git a/src/volume.ts b/src/volume.ts index b79fa1d13..1baaa0442 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) { - 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 { + 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;