Skip to content

Commit a1772b2

Browse files
feat: implement lutimes (#1066)
* feat: implement lutimes * fix: lutimes/lutimesSync not exported on fs
1 parent 18f4abe commit a1772b2

File tree

4 files changed

+90
-7
lines changed

4 files changed

+90
-7
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { create } from '../util';
2+
3+
describe('lutimesSync', () => {
4+
it('should be able to lutimes symlinks regardless of their permissions', () => {
5+
const perms = [
6+
0o777, // rwx
7+
0o666, // rw
8+
0o555, // rx
9+
0o444, // r
10+
0o333, // wx
11+
0o222, // w
12+
0o111, // x
13+
0o000, // none
14+
];
15+
// Check for directories
16+
perms.forEach(perm => {
17+
const vol = create({ '/target': 'test' });
18+
vol.symlinkSync('/target', '/test');
19+
expect(() => {
20+
vol.lutimesSync('/test', 0, 0);
21+
}).not.toThrow();
22+
});
23+
});
24+
25+
it('should set atime and mtime on the link itself, not the target', () => {
26+
const vol = create({ '/target': 'test' });
27+
vol.symlinkSync('/target', '/test');
28+
vol.lutimesSync('/test', new Date(1), new Date(2));
29+
const linkStats = vol.lstatSync('/test');
30+
const targetStats = vol.statSync('/target');
31+
32+
expect(linkStats.atime).toEqual(new Date(1));
33+
expect(linkStats.mtime).toEqual(new Date(2));
34+
35+
expect(targetStats.atime).not.toEqual(new Date(1));
36+
expect(targetStats.mtime).not.toEqual(new Date(2));
37+
});
38+
39+
it("should throw ENOENT when target doesn't exist", () => {
40+
const vol = create({ '/target': 'test' });
41+
// Don't create symlink this time
42+
expect(() => {
43+
vol.lutimesSync('/test', 0, 0);
44+
}).toThrow(/ENOENT/);
45+
});
46+
47+
it('should throw EACCES when containing directory has insufficient permissions', () => {
48+
const vol = create({ '/target': 'test' });
49+
vol.mkdirSync('/foo');
50+
vol.symlinkSync('/target', '/foo/test');
51+
vol.chmodSync('/foo', 0o666); // rw
52+
expect(() => {
53+
vol.lutimesSync('/foo/test', 0, 0);
54+
}).toThrow(/EACCES/);
55+
});
56+
57+
it('should throw EACCES when intermediate directory has insufficient permissions', () => {
58+
const vol = create({ '/target': 'test' });
59+
vol.mkdirSync('/foo');
60+
vol.symlinkSync('/target', '/foo/test');
61+
vol.chmodSync('/', 0o666); // rw
62+
expect(() => {
63+
vol.lutimesSync('/foo/test', 0, 0);
64+
}).toThrow(/EACCES/);
65+
});
66+
});

src/node/lists/fsCallbackApiList.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const fsCallbackApiList: Array<keyof FsCallbackApi> = [
3939
'unlink',
4040
'unwatchFile',
4141
'utimes',
42+
'lutimes',
4243
'watch',
4344
'watchFile',
4445
'write',

src/node/lists/fsSynchronousApiList.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ export const fsSynchronousApiList: Array<keyof FsSynchronousApi> = [
3636
'truncateSync',
3737
'unlinkSync',
3838
'utimesSync',
39+
'lutimesSync',
3940
'writeFileSync',
4041
'writeSync',
4142
'writevSync',
4243

4344
// 'cpSync',
44-
// 'lutimesSync',
4545
// 'statfsSync',
4646
];

src/volume.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1742,19 +1742,37 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
17421742
this.wrapAsync(this.futimesBase, [fd, toUnixTimestamp(atime), toUnixTimestamp(mtime)], callback);
17431743
}
17441744

1745-
private utimesBase(filename: string, atime: number, mtime: number) {
1746-
const link = this.getResolvedLinkOrThrow(filename, 'utimes');
1745+
private utimesBase(filename: string, atime: number, mtime: number, followSymlinks: boolean = true) {
1746+
const link = followSymlinks
1747+
? this.getResolvedLinkOrThrow(filename, 'utimes')
1748+
: this.getLinkOrThrow(filename, 'lutimes');
17471749
const node = link.getNode();
17481750
node.atime = new Date(atime * 1000);
17491751
node.mtime = new Date(mtime * 1000);
17501752
}
17511753

17521754
utimesSync(path: PathLike, atime: TTime, mtime: TTime) {
1753-
this.utimesBase(pathToFilename(path), toUnixTimestamp(atime), toUnixTimestamp(mtime));
1755+
this.utimesBase(pathToFilename(path), toUnixTimestamp(atime), toUnixTimestamp(mtime), true);
17541756
}
17551757

17561758
utimes(path: PathLike, atime: TTime, mtime: TTime, callback: TCallback<void>) {
1757-
this.wrapAsync(this.utimesBase, [pathToFilename(path), toUnixTimestamp(atime), toUnixTimestamp(mtime)], callback);
1759+
this.wrapAsync(
1760+
this.utimesBase,
1761+
[pathToFilename(path), toUnixTimestamp(atime), toUnixTimestamp(mtime), true],
1762+
callback,
1763+
);
1764+
}
1765+
1766+
lutimesSync(path: PathLike, atime: TTime, mtime: TTime): void {
1767+
this.utimesBase(pathToFilename(path), toUnixTimestamp(atime), toUnixTimestamp(mtime), false);
1768+
}
1769+
1770+
lutimes(path: PathLike, atime: TTime, mtime: TTime, callback: TCallback<void>): void {
1771+
this.wrapAsync(
1772+
this.utimesBase,
1773+
[pathToFilename(path), toUnixTimestamp(atime), toUnixTimestamp(mtime), false],
1774+
callback,
1775+
);
17581776
}
17591777

17601778
private mkdirBase(filename: string, modeNum: number) {
@@ -2130,11 +2148,9 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
21302148
}
21312149

21322150
public cpSync: FsSynchronousApi['cpSync'] = notImplemented;
2133-
public lutimesSync: FsSynchronousApi['lutimesSync'] = notImplemented;
21342151
public statfsSync: FsSynchronousApi['statfsSync'] = notImplemented;
21352152

21362153
public cp: FsCallbackApi['cp'] = notImplemented;
2137-
public lutimes: FsCallbackApi['lutimes'] = notImplemented;
21382154
public statfs: FsCallbackApi['statfs'] = notImplemented;
21392155
public openAsBlob: FsCallbackApi['openAsBlob'] = notImplemented;
21402156

0 commit comments

Comments
 (0)