Skip to content

Commit d466ab6

Browse files
committed
fix: fix bug on utimes, futimes, add support of lutimes
lutimes is available on Nodejs v14.5.0+ and v12.19.0, our CI matrix covers both versions. closes #365
1 parent 39bdc3f commit d466ab6

6 files changed

+535
-212
lines changed

lib/binding.js

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const Directory = require('./directory.js');
77
const SymbolicLink = require('./symlink.js');
88
const {FSError} = require('./error.js');
99
const constants = require('constants');
10-
const getPathParts = require('./filesystem.js').getPathParts;
10+
const {getPathParts, getRealPath} = require('./filesystem.js');
1111

1212
const MODE_TO_KTYPE = {
1313
[constants.S_IFREG]: constants.UV_DIRENT_FILE,
@@ -260,10 +260,8 @@ Binding.prototype.realpath = function (filepath, encoding, callback, ctx) {
260260
throw new FSError('ENOENT', filepath);
261261
}
262262

263-
if (process.platform === 'win32' && realPath.startsWith('\\\\?\\')) {
264-
// Remove win32 file namespace prefix \\?\
265-
realPath = realPath.slice(4);
266-
}
263+
// Remove win32 file namespace prefix \\?\
264+
realPath = getRealPath(realPath);
267265

268266
if (encoding === 'buffer') {
269267
realPath = Buffer.from(realPath);
@@ -1073,12 +1071,45 @@ Binding.prototype.unlink = function (pathname, callback, ctx) {
10731071
Binding.prototype.utimes = function (pathname, atime, mtime, callback, ctx) {
10741072
markSyscall(ctx, 'utimes');
10751073

1074+
return maybeCallback(normalizeCallback(callback), ctx, this, function () {
1075+
let filepath = deBuffer(pathname);
1076+
let item = this._system.getItem(filepath);
1077+
let links = 0;
1078+
while (item instanceof SymbolicLink) {
1079+
if (links > MAX_LINKS) {
1080+
throw new FSError('ELOOP', filepath);
1081+
}
1082+
filepath = path.resolve(path.dirname(filepath), item.getPath());
1083+
item = this._system.getItem(filepath);
1084+
++links;
1085+
}
1086+
if (!item) {
1087+
throw new FSError('ENOENT', pathname);
1088+
}
1089+
item.setATime(new Date(atime * 1000));
1090+
item.setMTime(new Date(mtime * 1000));
1091+
});
1092+
};
1093+
1094+
/**
1095+
* Update timestamps.
1096+
* @param {string} pathname Path to item.
1097+
* @param {number} atime Access time (in seconds).
1098+
* @param {number} mtime Modification time (in seconds).
1099+
* @param {function(Error)} callback Optional callback.
1100+
* @param {object} ctx Context object (optional), only for nodejs v10+.
1101+
* @return {*} The return if no callback is provided.
1102+
*/
1103+
Binding.prototype.lutimes = function (pathname, atime, mtime, callback, ctx) {
1104+
markSyscall(ctx, 'utimes');
1105+
10761106
return maybeCallback(normalizeCallback(callback), ctx, this, function () {
10771107
pathname = deBuffer(pathname);
10781108
const item = this._system.getItem(pathname);
10791109
if (!item) {
10801110
throw new FSError('ENOENT', pathname);
10811111
}
1112+
// lutimes doesn't follow symlink
10821113
item.setATime(new Date(atime * 1000));
10831114
item.setMTime(new Date(mtime * 1000));
10841115
});
@@ -1098,7 +1129,17 @@ Binding.prototype.futimes = function (fd, atime, mtime, callback, ctx) {
10981129

10991130
return maybeCallback(normalizeCallback(callback), ctx, this, function () {
11001131
const descriptor = this.getDescriptorById(fd);
1101-
const item = descriptor.getItem();
1132+
let item = descriptor.getItem();
1133+
let filepath = this._system.getFilepath(item);
1134+
let links = 0;
1135+
while (item instanceof SymbolicLink) {
1136+
if (links > MAX_LINKS) {
1137+
throw new FSError('ELOOP', filepath);
1138+
}
1139+
filepath = path.resolve(path.dirname(filepath), item.getPath());
1140+
item = this._system.getItem(filepath);
1141+
++links;
1142+
}
11021143
item.setATime(new Date(atime * 1000));
11031144
item.setMTime(new Date(mtime * 1000));
11041145
});

lib/filesystem.js

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,26 @@ const SymbolicLink = require('./symlink.js');
99

1010
const isWindows = process.platform === 'win32';
1111

12-
function toNamespacedPath(filePath) {
13-
return path.toNamespacedPath
14-
? path.toNamespacedPath(filePath)
15-
: path._makeLong(filePath);
12+
// on Win32, change filepath from \\?\c:\a\b to C:\a\b
13+
function getRealPath(filepath) {
14+
if (isWindows && filepath.startsWith('\\\\?\\')) {
15+
// Remove win32 file namespace prefix \\?\
16+
return filepath[4].toUpperCase() + filepath.slice(5);
17+
}
18+
return filepath;
1619
}
1720

1821
function getPathParts(filepath) {
19-
const parts = toNamespacedPath(path.resolve(filepath)).split(path.sep);
22+
// path.toNamespacedPath is only for Win32 system.
23+
// on other platform, it returns the path unmodified.
24+
const parts = path.toNamespacedPath(path.resolve(filepath)).split(path.sep);
2025
parts.shift();
2126
if (isWindows) {
2227
// parts currently looks like ['', '?', 'c:', ...]
2328
parts.shift();
2429
const q = parts.shift(); // should be '?'
30+
// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#win32-file-namespaces
31+
// Win32 File Namespaces prefix \\?\
2532
const base = '\\\\' + q + '\\' + parts.shift().toLowerCase();
2633
parts.unshift(base);
2734
}
@@ -130,6 +137,35 @@ FileSystem.prototype.getItem = function (filepath) {
130137
return item;
131138
};
132139

140+
function _getFilepath(item, itemPath, wanted) {
141+
if (item === wanted) {
142+
return itemPath;
143+
}
144+
if (item instanceof Directory) {
145+
for (const name of item.list()) {
146+
const got = _getFilepath(
147+
item.getItem(name),
148+
path.join(itemPath, name),
149+
wanted
150+
);
151+
if (got) {
152+
return got;
153+
}
154+
}
155+
}
156+
return null;
157+
}
158+
159+
/**
160+
* Get file path from a file system item.
161+
* @param {Item} item a file system item.
162+
* @return {string} file path for the item (or null if not found).
163+
*/
164+
FileSystem.prototype.getFilepath = function (item) {
165+
const namespacedPath = _getFilepath(this._root, isWindows ? '' : '/', item);
166+
return getRealPath(namespacedPath);
167+
};
168+
133169
/**
134170
* Populate a directory with an item.
135171
* @param {Directory} directory The directory to populate.
@@ -329,4 +365,4 @@ FileSystem.directory = function (config) {
329365
module.exports = FileSystem;
330366
exports = module.exports;
331367
exports.getPathParts = getPathParts;
332-
exports.toNamespacedPath = toNamespacedPath;
368+
exports.getRealPath = getRealPath;

lib/index.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ const {
1313
} = require('./readfilecontext.js');
1414
const fs = require('fs');
1515

16-
const toNamespacedPath = FileSystem.toNamespacedPath;
17-
1816
const realProcessProps = {
1917
cwd: process.cwd,
2018
chdir: process.chdir,
@@ -153,7 +151,7 @@ module.exports = function mock(config, options = {}) {
153151
},
154152
function chdir(directory) {
155153
if (realBinding._mockedBinding) {
156-
if (!fs.statSync(toNamespacedPath(directory)).isDirectory()) {
154+
if (!fs.statSync(path.toNamespacedPath(directory)).isDirectory()) {
157155
throw new FSError('ENOTDIR');
158156
}
159157
currentPath = path.resolve(currentPath, directory);

test/lib/filesystem.spec.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('FileSystem', function () {
4040
});
4141
});
4242

43-
describe('#getItem()', function () {
43+
describe('#getItem() #getFilepath()', function () {
4444
it('gets an item', function () {
4545
const system = FileSystem.create({
4646
'one/two/three.js': 'contents',
@@ -49,6 +49,7 @@ describe('FileSystem', function () {
4949
const filepath = path.join('one', 'two', 'three.js');
5050
const item = system.getItem(filepath);
5151
assert.instanceOf(item, File);
52+
assert.equal(system.getFilepath(item), path.resolve(filepath));
5253
});
5354

5455
it('returns null if not found', function () {
@@ -79,10 +80,18 @@ describe('FileSystem', function () {
7980
});
8081
const file = system.getItem(path.join('dir-link', 'a'));
8182
assert.instanceOf(file, File);
83+
assert.equal(
84+
system.getFilepath(file),
85+
path.resolve(path.join('b', 'c', 'dir', 'a'))
86+
);
8287

8388
const dir = system.getItem(path.join('dir-link', 'b'));
8489
assert.instanceOf(dir, Directory);
8590
assert.deepEqual(dir.list().sort(), ['c', 'd']);
91+
assert.equal(
92+
system.getFilepath(dir),
93+
path.resolve(path.join('b', 'c', 'dir', 'b'))
94+
);
8695
});
8796
});
8897
});

0 commit comments

Comments
 (0)