Skip to content

Commit

Permalink
feat: implement lutimes (#1066)
Browse files Browse the repository at this point in the history
* feat: implement lutimes

* fix: lutimes/lutimesSync not exported on fs
  • Loading branch information
BadIdeaException authored Oct 7, 2024
1 parent 18f4abe commit a1772b2
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 7 deletions.
66 changes: 66 additions & 0 deletions src/__tests__/volume/lutimesSync.test.ts
Original file line number Diff line number Diff line change
@@ -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/);
});
});
1 change: 1 addition & 0 deletions src/node/lists/fsCallbackApiList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const fsCallbackApiList: Array<keyof FsCallbackApi> = [
'unlink',
'unwatchFile',
'utimes',
'lutimes',
'watch',
'watchFile',
'write',
Expand Down
2 changes: 1 addition & 1 deletion src/node/lists/fsSynchronousApiList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ export const fsSynchronousApiList: Array<keyof FsSynchronousApi> = [
'truncateSync',
'unlinkSync',
'utimesSync',
'lutimesSync',
'writeFileSync',
'writeSync',
'writevSync',

// 'cpSync',
// 'lutimesSync',
// 'statfsSync',
];
28 changes: 22 additions & 6 deletions src/volume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;

Expand Down

0 comments on commit a1772b2

Please sign in to comment.