lts/lib/internal/fs/utils.js
'use strict';
const {
ArrayIsArray,
BigInt,
DateNow,
Error,
Number,
NumberIsFinite,
ObjectSetPrototypeOf,
ReflectOwnKeys,
Symbol,
} = primordials;
const { Buffer, kMaxLength } = require('buffer');
const {
codes: {
ERR_FS_INVALID_SYMLINK_TYPE,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_OPT_VALUE,
ERR_INVALID_OPT_VALUE_ENCODING,
ERR_OUT_OF_RANGE
},
hideStackFrames,
uvException
} = require('internal/errors');
const {
isArrayBufferView,
isUint8Array,
isDate,
isBigUint64Array
} = require('internal/util/types');
const { once } = require('internal/util');
const { toPathIfFileURL } = require('internal/url');
const {
validateInt32,
validateUint32
} = require('internal/validators');
const pathModule = require('path');
const kType = Symbol('type');
const kStats = Symbol('stats');
const {
O_APPEND,
O_CREAT,
O_EXCL,
O_RDONLY,
O_RDWR,
O_SYNC,
O_TRUNC,
O_WRONLY,
S_IFBLK,
S_IFCHR,
S_IFDIR,
S_IFIFO,
S_IFLNK,
S_IFMT,
S_IFREG,
S_IFSOCK,
UV_FS_SYMLINK_DIR,
UV_FS_SYMLINK_JUNCTION,
UV_DIRENT_UNKNOWN,
UV_DIRENT_FILE,
UV_DIRENT_DIR,
UV_DIRENT_LINK,
UV_DIRENT_FIFO,
UV_DIRENT_SOCKET,
UV_DIRENT_CHAR,
UV_DIRENT_BLOCK
} = internalBinding('constants').fs;
const isWindows = process.platform === 'win32';
let fs;
function lazyLoadFs() {
if (!fs) {
fs = require('fs');
}
return fs;
}
function assertEncoding(encoding) {
if (encoding && !Buffer.isEncoding(encoding)) {
throw new ERR_INVALID_OPT_VALUE_ENCODING(encoding);
}
}
class Dirent {
constructor(name, type) {
this.name = name;
this[kType] = type;
}
isDirectory() {
return this[kType] === UV_DIRENT_DIR;
}
isFile() {
return this[kType] === UV_DIRENT_FILE;
}
isBlockDevice() {
return this[kType] === UV_DIRENT_BLOCK;
}
isCharacterDevice() {
return this[kType] === UV_DIRENT_CHAR;
}
isSymbolicLink() {
return this[kType] === UV_DIRENT_LINK;
}
isFIFO() {
return this[kType] === UV_DIRENT_FIFO;
}
isSocket() {
return this[kType] === UV_DIRENT_SOCKET;
}
}
class DirentFromStats extends Dirent {
constructor(name, stats) {
super(name, null);
this[kStats] = stats;
}
}
for (const name of ReflectOwnKeys(Dirent.prototype)) {
if (name === 'constructor') {
continue;
}
DirentFromStats.prototype[name] = function() {
return this[kStats][name]();
};
}
function copyObject(source) {
const target = {};
for (const key in source)
target[key] = source[key];
return target;
}
const bufferSep = Buffer.from(pathModule.sep);
function join(path, name) {
if ((typeof path === 'string' || isUint8Array(path)) &&
name === undefined) {
return path;
}
if (typeof path === 'string' && isUint8Array(name)) {
const pathBuffer = Buffer.from(pathModule.join(path, pathModule.sep));
return Buffer.concat([pathBuffer, name]);
}
if (typeof path === 'string' && typeof name === 'string') {
return pathModule.join(path, name);
}
if (isUint8Array(path) && isUint8Array(name)) {
return Buffer.concat([path, bufferSep, name]);
}
throw new ERR_INVALID_ARG_TYPE(
'path', ['string', 'Buffer'], path);
}
function getDirents(path, [names, types], callback) {
let i;
if (typeof callback === 'function') {
const len = names.length;
let toFinish = 0;
callback = once(callback);
for (i = 0; i < len; i++) {
const type = types[i];
if (type === UV_DIRENT_UNKNOWN) {
const name = names[i];
const idx = i;
toFinish++;
let filepath;
try {
filepath = join(path, name);
} catch (err) {
callback(err);
return;
}
lazyLoadFs().lstat(filepath, (err, stats) => {
if (err) {
callback(err);
return;
}
names[idx] = new DirentFromStats(name, stats);
if (--toFinish === 0) {
callback(null, names);
}
});
} else {
names[i] = new Dirent(names[i], types[i]);
}
}
if (toFinish === 0) {
callback(null, names);
}
} else {
const len = names.length;
for (i = 0; i < len; i++) {
names[i] = getDirent(path, names[i], types[i]);
}
return names;
}
}
function getDirent(path, name, type, callback) {
if (typeof callback === 'function') {
if (type === UV_DIRENT_UNKNOWN) {
let filepath;
try {
filepath = join(path, name);
} catch (err) {
callback(err);
return;
}
lazyLoadFs().lstat(filepath, (err, stats) => {
if (err) {
callback(err);
return;
}
callback(null, new DirentFromStats(name, stats));
});
} else {
callback(null, new Dirent(name, type));
}
} else if (type === UV_DIRENT_UNKNOWN) {
const stats = lazyLoadFs().lstatSync(join(path, name));
return new DirentFromStats(name, stats);
} else {
return new Dirent(name, type);
}
}
function getOptions(options, defaultOptions) {
if (options === null || options === undefined ||
typeof options === 'function') {
return defaultOptions;
}
if (typeof options === 'string') {
defaultOptions = { ...defaultOptions };
defaultOptions.encoding = options;
options = defaultOptions;
} else if (typeof options !== 'object') {
throw new ERR_INVALID_ARG_TYPE('options', ['string', 'Object'], options);
}
if (options.encoding !== 'buffer')
assertEncoding(options.encoding);
return options;
}
function handleErrorFromBinding(ctx) {
if (ctx.errno !== undefined) { // libuv error numbers
const err = uvException(ctx);
// eslint-disable-next-line no-restricted-syntax
Error.captureStackTrace(err, handleErrorFromBinding);
throw err;
}
if (ctx.error !== undefined) { // Errors created in C++ land.
// TODO(joyeecheung): currently, ctx.error are encoding errors
// usually caused by memory problems. We need to figure out proper error
// code(s) for this.
// eslint-disable-next-line no-restricted-syntax
Error.captureStackTrace(ctx.error, handleErrorFromBinding);
throw ctx.error;
}
}
// Check if the path contains null types if it is a string nor Uint8Array,
// otherwise return silently.
const nullCheck = hideStackFrames((path, propName, throwError = true) => {
const pathIsString = typeof path === 'string';
const pathIsUint8Array = isUint8Array(path);
// We can only perform meaningful checks on strings and Uint8Arrays.
if ((!pathIsString && !pathIsUint8Array) ||
(pathIsString && !path.includes('\u0000')) ||
(pathIsUint8Array && !path.includes(0))) {
return;
}
const err = new ERR_INVALID_ARG_VALUE(
propName,
path,
'must be a string or Uint8Array without null bytes'
);
if (throwError) {
throw err;
}
return err;
});
function preprocessSymlinkDestination(path, type, linkPath) {
if (!isWindows) {
// No preprocessing is needed on Unix.
return path;
}
if (type === 'junction') {
// Junctions paths need to be absolute and \\?\-prefixed.
// A relative target is relative to the link's parent directory.
path = pathModule.resolve(linkPath, '..', path);
return pathModule.toNamespacedPath(path);
}
if (pathModule.isAbsolute(path)) {
// If the path is absolute, use the \\?\-prefix to enable long filenames
return pathModule.toNamespacedPath(path);
}
// Windows symlinks don't tolerate forward slashes.
return ('' + path).replace(/\//g, '\\');
}
// Constructor for file stats.
function StatsBase(dev, mode, nlink, uid, gid, rdev, blksize,
ino, size, blocks) {
this.dev = dev;
this.mode = mode;
this.nlink = nlink;
this.uid = uid;
this.gid = gid;
this.rdev = rdev;
this.blksize = blksize;
this.ino = ino;
this.size = size;
this.blocks = blocks;
}
StatsBase.prototype.isDirectory = function() {
return this._checkModeProperty(S_IFDIR);
};
StatsBase.prototype.isFile = function() {
return this._checkModeProperty(S_IFREG);
};
StatsBase.prototype.isBlockDevice = function() {
return this._checkModeProperty(S_IFBLK);
};
StatsBase.prototype.isCharacterDevice = function() {
return this._checkModeProperty(S_IFCHR);
};
StatsBase.prototype.isSymbolicLink = function() {
return this._checkModeProperty(S_IFLNK);
};
StatsBase.prototype.isFIFO = function() {
return this._checkModeProperty(S_IFIFO);
};
StatsBase.prototype.isSocket = function() {
return this._checkModeProperty(S_IFSOCK);
};
const kNsPerMsBigInt = 10n ** 6n;
const kNsPerSecBigInt = 10n ** 9n;
const kMsPerSec = 10 ** 3;
const kNsPerMs = 10 ** 6;
function msFromTimeSpec(sec, nsec) {
return sec * kMsPerSec + nsec / kNsPerMs;
}
function nsFromTimeSpecBigInt(sec, nsec) {
return sec * kNsPerSecBigInt + nsec;
}
// The Date constructor performs Math.floor() to the timestamp.
// https://www.ecma-international.org/ecma-262/#sec-timeclip
// Since there may be a precision loss when the timestamp is
// converted to a floating point number, we manually round
// the timestamp here before passing it to Date().
// Refs: https://github.com/nodejs/node/pull/12607
function dateFromMs(ms) {
return new Date(Number(ms) + 0.5);
}
function BigIntStats(dev, mode, nlink, uid, gid, rdev, blksize,
ino, size, blocks,
atimeNs, mtimeNs, ctimeNs, birthtimeNs) {
StatsBase.call(this, dev, mode, nlink, uid, gid, rdev, blksize,
ino, size, blocks);
this.atimeMs = atimeNs / kNsPerMsBigInt;
this.mtimeMs = mtimeNs / kNsPerMsBigInt;
this.ctimeMs = ctimeNs / kNsPerMsBigInt;
this.birthtimeMs = birthtimeNs / kNsPerMsBigInt;
this.atimeNs = atimeNs;
this.mtimeNs = mtimeNs;
this.ctimeNs = ctimeNs;
this.birthtimeNs = birthtimeNs;
this.atime = dateFromMs(this.atimeMs);
this.mtime = dateFromMs(this.mtimeMs);
this.ctime = dateFromMs(this.ctimeMs);
this.birthtime = dateFromMs(this.birthtimeMs);
}
ObjectSetPrototypeOf(BigIntStats.prototype, StatsBase.prototype);
ObjectSetPrototypeOf(BigIntStats, StatsBase);
BigIntStats.prototype._checkModeProperty = function(property) {
if (isWindows && (property === S_IFIFO || property === S_IFBLK ||
property === S_IFSOCK)) {
return false; // Some types are not available on Windows
}
return (this.mode & BigInt(S_IFMT)) === BigInt(property);
};
function Stats(dev, mode, nlink, uid, gid, rdev, blksize,
ino, size, blocks,
atimeMs, mtimeMs, ctimeMs, birthtimeMs) {
StatsBase.call(this, dev, mode, nlink, uid, gid, rdev, blksize,
ino, size, blocks);
this.atimeMs = atimeMs;
this.mtimeMs = mtimeMs;
this.ctimeMs = ctimeMs;
this.birthtimeMs = birthtimeMs;
this.atime = dateFromMs(atimeMs);
this.mtime = dateFromMs(mtimeMs);
this.ctime = dateFromMs(ctimeMs);
this.birthtime = dateFromMs(birthtimeMs);
}
ObjectSetPrototypeOf(Stats.prototype, StatsBase.prototype);
ObjectSetPrototypeOf(Stats, StatsBase);
// HACK: Workaround for https://github.com/standard-things/esm/issues/821.
// TODO(ronag): Remove this as soon as `esm` publishes a fixed version.
Stats.prototype.isFile = StatsBase.prototype.isFile;
Stats.prototype._checkModeProperty = function(property) {
if (isWindows && (property === S_IFIFO || property === S_IFBLK ||
property === S_IFSOCK)) {
return false; // Some types are not available on Windows
}
return (this.mode & S_IFMT) === property;
};
function getStatsFromBinding(stats, offset = 0) {
if (isBigUint64Array(stats)) {
return new BigIntStats(
stats[0 + offset], stats[1 + offset], stats[2 + offset],
stats[3 + offset], stats[4 + offset], stats[5 + offset],
stats[6 + offset], stats[7 + offset], stats[8 + offset],
stats[9 + offset],
nsFromTimeSpecBigInt(stats[10 + offset], stats[11 + offset]),
nsFromTimeSpecBigInt(stats[12 + offset], stats[13 + offset]),
nsFromTimeSpecBigInt(stats[14 + offset], stats[15 + offset]),
nsFromTimeSpecBigInt(stats[16 + offset], stats[17 + offset])
);
}
return new Stats(
stats[0 + offset], stats[1 + offset], stats[2 + offset],
stats[3 + offset], stats[4 + offset], stats[5 + offset],
stats[6 + offset], stats[7 + offset], stats[8 + offset],
stats[9 + offset],
msFromTimeSpec(stats[10 + offset], stats[11 + offset]),
msFromTimeSpec(stats[12 + offset], stats[13 + offset]),
msFromTimeSpec(stats[14 + offset], stats[15 + offset]),
msFromTimeSpec(stats[16 + offset], stats[17 + offset])
);
}
function stringToFlags(flags) {
if (typeof flags === 'number') {
return flags;
}
switch (flags) {
case 'r' : return O_RDONLY;
case 'rs' : // Fall through.
case 'sr' : return O_RDONLY | O_SYNC;
case 'r+' : return O_RDWR;
case 'rs+' : // Fall through.
case 'sr+' : return O_RDWR | O_SYNC;
case 'w' : return O_TRUNC | O_CREAT | O_WRONLY;
case 'wx' : // Fall through.
case 'xw' : return O_TRUNC | O_CREAT | O_WRONLY | O_EXCL;
case 'w+' : return O_TRUNC | O_CREAT | O_RDWR;
case 'wx+': // Fall through.
case 'xw+': return O_TRUNC | O_CREAT | O_RDWR | O_EXCL;
case 'a' : return O_APPEND | O_CREAT | O_WRONLY;
case 'ax' : // Fall through.
case 'xa' : return O_APPEND | O_CREAT | O_WRONLY | O_EXCL;
case 'as' : // Fall through.
case 'sa' : return O_APPEND | O_CREAT | O_WRONLY | O_SYNC;
case 'a+' : return O_APPEND | O_CREAT | O_RDWR;
case 'ax+': // Fall through.
case 'xa+': return O_APPEND | O_CREAT | O_RDWR | O_EXCL;
case 'as+': // Fall through.
case 'sa+': return O_APPEND | O_CREAT | O_RDWR | O_SYNC;
}
throw new ERR_INVALID_OPT_VALUE('flags', flags);
}
const stringToSymlinkType = hideStackFrames((type) => {
let flags = 0;
if (typeof type === 'string') {
switch (type) {
case 'dir':
flags |= UV_FS_SYMLINK_DIR;
break;
case 'junction':
flags |= UV_FS_SYMLINK_JUNCTION;
break;
case 'file':
break;
default:
throw new ERR_FS_INVALID_SYMLINK_TYPE(type);
}
}
return flags;
});
// converts Date or number to a fractional UNIX timestamp
function toUnixTimestamp(time, name = 'time') {
// eslint-disable-next-line eqeqeq
if (typeof time === 'string' && +time == time) {
return +time;
}
if (NumberIsFinite(time)) {
if (time < 0) {
return DateNow() / 1000;
}
return time;
}
if (isDate(time)) {
// Convert to 123.456 UNIX timestamp
return time.getTime() / 1000;
}
throw new ERR_INVALID_ARG_TYPE(name, ['Date', 'Time in seconds'], time);
}
const validateOffsetLengthRead = hideStackFrames(
(offset, length, bufferLength) => {
if (offset < 0) {
throw new ERR_OUT_OF_RANGE('offset', '>= 0', offset);
}
if (length < 0) {
throw new ERR_OUT_OF_RANGE('length', '>= 0', length);
}
if (offset + length > bufferLength) {
throw new ERR_OUT_OF_RANGE('length',
`<= ${bufferLength - offset}`, length);
}
}
);
const validateOffsetLengthWrite = hideStackFrames(
(offset, length, byteLength) => {
if (offset > byteLength) {
throw new ERR_OUT_OF_RANGE('offset', `<= ${byteLength}`, offset);
}
const max = byteLength > kMaxLength ? kMaxLength : byteLength;
if (length > max - offset) {
throw new ERR_OUT_OF_RANGE('length', `<= ${max - offset}`, length);
}
}
);
const validatePath = hideStackFrames((path, propName = 'path') => {
if (typeof path !== 'string' && !isUint8Array(path)) {
throw new ERR_INVALID_ARG_TYPE(propName, ['string', 'Buffer', 'URL'], path);
}
const err = nullCheck(path, propName, false);
if (err !== undefined) {
throw err;
}
});
const getValidatedPath = hideStackFrames((fileURLOrPath, propName = 'path') => {
const path = toPathIfFileURL(fileURLOrPath);
validatePath(path, propName);
return path;
});
const validateBufferArray = hideStackFrames((buffers, propName = 'buffers') => {
if (!ArrayIsArray(buffers))
throw new ERR_INVALID_ARG_TYPE(propName, 'ArrayBufferView[]', buffers);
for (let i = 0; i < buffers.length; i++) {
if (!isArrayBufferView(buffers[i]))
throw new ERR_INVALID_ARG_TYPE(propName, 'ArrayBufferView[]', buffers);
}
return buffers;
});
let nonPortableTemplateWarn = true;
function warnOnNonPortableTemplate(template) {
// Template strings passed to the mkdtemp() family of functions should not
// end with 'X' because they are handled inconsistently across platforms.
if (nonPortableTemplateWarn && template.endsWith('X')) {
process.emitWarning('mkdtemp() templates ending with X are not portable. ' +
'For details see: https://nodejs.org/api/fs.html');
nonPortableTemplateWarn = false;
}
}
const defaultRmdirOptions = {
retryDelay: 100,
maxRetries: 0,
recursive: false,
};
const validateRmdirOptions = hideStackFrames((options) => {
if (options === undefined)
return defaultRmdirOptions;
if (options === null || typeof options !== 'object')
throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
options = { ...defaultRmdirOptions, ...options };
if (typeof options.recursive !== 'boolean')
throw new ERR_INVALID_ARG_TYPE('recursive', 'boolean', options.recursive);
validateInt32(options.retryDelay, 'retryDelay', 0);
validateUint32(options.maxRetries, 'maxRetries');
return options;
});
module.exports = {
assertEncoding,
BigIntStats, // for testing
copyObject,
Dirent,
getDirent,
getDirents,
getOptions,
getValidatedPath,
handleErrorFromBinding,
nullCheck,
preprocessSymlinkDestination,
realpathCacheKey: Symbol('realpathCacheKey'),
getStatsFromBinding,
stringToFlags,
stringToSymlinkType,
Stats,
toUnixTimestamp,
validateBufferArray,
validateOffsetLengthRead,
validateOffsetLengthWrite,
validatePath,
validateRmdirOptions,
warnOnNonPortableTemplate
};