const crypto = require('crypto');
const errno = require('errno');
const io = require('io');
const proc = require('proc');
/**
* Return from {module:fs.stat} function holding information of a file node
*
* @typedef {object} StatBuf
* @property {number} gid Owner group id
* @property {number} mode File type and access mode
* @property {number} size Size in bytes
* @property {TimeSpec} time Time statistics
* @property {number} uid Owner user id
*
* @see {@link module:fs.S_IFMT}
* @see {@link module:fs.S_IFBLK}
* @see {@link module:fs.S_IFCHR}
* @see {@link module:fs.S_IFDIR}
* @see {@link module:fs.S_IFIFO}
* @see {@link module:fs.S_IFLNK}
* @see {@link module:fs.S_IFREG}
* @see {@link module:fs.S_IFSOC}
*/
/**
* Time statistics of a file node (with a resolution of one second)
*
* @typedef {object} TimeSpec
* @property {number} access Last access time
* @property {number} creation Creation time
* @property {number} modification Last modification time
*/
/**
* Callback for {@link module:fs.list_dir} method
*
* @callback ListDirCallback
* @param {string} item Directory item (file name)
* @param {number} index Index of item in directory
* @returns {boolean|undefined} Return `false` to stop listing
*/
const decoder = new TextDecoder();
/**
* @exports fs
* @readonly
* @enum {number}
*/
const fs = {
/* stat flags */
/** Used to & return of {@link module:fs.stat} to extract inode type */
S_IFMT: 0170000,
/** File node type: block device */
S_IFBLK: 0060000,
/** File node type: char device */
S_IFCHR: 0020000,
/** File node type: directory */
S_IFDIR: 0040000,
/** File node type: FIFO */
S_IFIFO: 0010000,
/** File node type: symbolic link */
S_IFLNK: 0120000,
/** File node type: regular file */
S_IFREG: 0100000,
/** File node type: socket */
S_IFSOC: 0140000,
};
/**
* Get the filename part of a path
*
* @param {string} path
* @returns {string}
*/
fs.basename = function (path) {
return path.substring(1 + path.lastIndexOf('/'));
};
/**
* Change file owner and group of a file or symbolic link
*
* @param {number} uid The new owner
* @param {number} [gid] The new group (default is to leave untouched)
* @returns {0}
* @throws {SysError}
*/
fs.chown = function (path, uid, gid) {
if (gid === undefined) {
gid = fs.stat(path).gid;
}
return j.lchown(path, uid, gid);
};
/**
* Copy file.
*
* Note that if the copy fails, the target file is left in an undefined state.
*
* @param {string} from Source file path
* @param {string} to Destination file path
*
* @param {number} [mode=same as source file]
* File creation mode.
*
* Note that if the file exists its mode is left untouched.
*
* @returns {void}
* @throws {SysError}
*/
fs.copy_file = function (from, to, mode) {
if (mode === undefined) {
mode = fs.stat(from).mode & 07777;
}
var fdFrom;
var fdTo;
try {
fdFrom = io.open(from);
fdTo = io.truncate(to, mode);
const buf = new Uint8Array(4096);
var count;
while ((count = io.read(fdFrom, buf, buf.length)) !== 0) {
io.write(fdTo, buf, count);
}
} catch (err) {
throw err;
} finally {
io.close(fdFrom, false);
io.close(fdTo, false);
}
};
/**
* Create a temporary file with a random name
*
* @param {string} [contents=''] Initial file contents
* @param {number} [mode=0600] Creation mode
* @returns {string} The temporary file path
* @throws {SysError}
*/
fs.create_temp_file = function (contents, mode) {
if (typeof contents === 'number') {
mode = contents;
contents = undefined;
}
contents = contents || '';
mode = Number(mode || 0600);
const rnd = crypto.get_random_bytes(4);
const filename =
'/tmp/joshi_' +
proc.getpid().toString(16) +
'_' +
rnd[0].toString(16) +
rnd[1].toString(16) +
rnd[2].toString(16) +
rnd[3].toString(16);
fs.write_file(filename, contents, mode);
return filename;
};
/**
* Get the directory part of a path
*
* @param {string} path
* @returns {string} A directory or '.' if none was present in parameter
*/
fs.dirname = function (path) {
const i = path.lastIndexOf('/');
if (i === -1) {
return '.';
} else if (i === 0) {
return '/';
} else {
return path.substring(0, i);
}
};
/**
* Check if a file path exists
*
* @param {string} pathname
* @returns {boolean}
* @throws {SysError} If anything goes wrong
*/
fs.exists = function (pathname) {
try {
fs.stat(pathname);
return true;
} catch (err) {
if (err.errno === errno.ENOENT) {
return false;
}
throw err;
}
};
/**
* Check if a path points to a block device
*
* @param {string} pathname
* @returns {boolean}
* @throws {SysError} If anything goes wrong or the path does not exist
*/
fs.is_block_device = function (pathname) {
return (fs.stat(pathname).mode & fs.S_IFMT) === fs.S_IFBLK;
};
/**
* Check if a path points to a char device
*
* @param {string} pathname
* @returns {boolean}
* @throws {SysError} If anything goes wrong or the path does not exist
*/
fs.is_char_device = function (pathname) {
return (fs.stat(pathname).mode & fs.S_IFMT) === fs.S_IFCHR;
};
/**
* Check if a path points to a directory
*
* @param {string} pathname
* @returns {boolean}
* @throws {SysError} If anything goes wrong or the path does not exist
*/
fs.is_directory = function (pathname) {
return (fs.stat(pathname).mode & fs.S_IFMT) === fs.S_IFDIR;
};
/**
* Check if a file is executable by current process given its effective gid and
* uid.
*
* @param {string} pathname
* @returns {boolean}
* @throws {SysError} If anything goes wrong or the path does not exist
*/
fs.is_executable = function (pathname) {
return matches_mode(pathname, 01);
};
/**
* Check if a path points to a FIFO
*
* @param {string} pathname
* @returns {boolean}
* @throws {SysError} If anything goes wrong or the path does not exist
*/
fs.is_fifo = function (pathname) {
return (fs.stat(pathname).mode & fs.S_IFMT) === fs.S_IFIFO;
};
/**
* Check if a path points to a regular file
*
* @param {string} pathname
* @returns {boolean}
* @throws {SysError} If anything goes wrong or the path does not exist
*/
fs.is_file = function (pathname) {
return (fs.stat(pathname).mode & fs.S_IFMT) === fs.S_IFREG;
};
/**
* Check if a path points to a symbolic link
*
* @param {string} pathname
* @returns {boolean}
* @throws {SysError} If anything goes wrong or the path does not exist
*/
fs.is_link = function (pathname) {
return (fs.stat(pathname).mode & fs.S_IFMT) === fs.S_IFLNK;
};
/**
* Check if a file is readable by current process given its effective gid and
* uid.
*
* @param {string} pathname
* @returns {boolean}
* @throws {SysError} If anything goes wrong or the path does not exist
*/
fs.is_readable = function (pathname) {
return matches_mode(pathname, 04);
};
/**
* Check if a path points to a socket
*
* @param {string} pathname
* @returns {boolean}
* @throws {SysError} If anything goes wrong or the path does not exist
*/
fs.is_socket = function (pathname) {
return (fs.stat(pathname).mode & fs.S_IFMT) === fs.S_IFSOCK;
};
/**
* Check if a file is writable by current process given its effective gid and
* uid.
*
* @param {string} pathname
* @returns {boolean}
* @throws {SysError} If anything goes wrong or the path does not exist
*/
fs.is_writable = function (pathname) {
return matches_mode(pathname, 02);
};
/**
* Join several path parts and normalize the result
*
* @param {...string} paths Path parts to join
* @returns {string}
* @throws {SysError}
* @see {module:fs.normalize_path}
*/
fs.join = function () {
var path = arguments[0];
for (var i = 1; i < arguments.length; i++) {
if (arguments[i][0] === '/') {
errno.fail(errno.EINVAL);
}
if (!path.endsWith('/')) {
path += '/';
}
path += arguments[i];
}
return fs.normalize_path(path);
};
/**
* List items of a directory (not including `.` and `..`).
*
* The order of listing is determined by the underlying filesystem.
*
* @param {string} name Path of directory
*
* @param {ListDirCallback} [callback]
* A callback function invoked for each directory item. Use this idiom to avoid
* having to wait for the whole listing to finish.
*
* @return {boolean|string[]}
* The list of items or, if a callback is provided, a boolean indicating if the
* listing finished (true) or was cancelled (false).
*
* @throws {SysError}
*/
fs.list_dir = function (name, callback) {
const items = callback ? undefined : [];
var finished = false;
var dirp;
try {
const cb = callback
? callback
: function (item) {
items.push(item);
};
dirp = j.opendir(name);
var index = 0;
while (true) {
const dirent = j.readdir(dirp);
const name = dirent.d_name;
if (name === '.' || name === '..') {
continue;
}
if (cb(name, index++) === false) {
break;
}
}
} catch (err) {
if (err.errno) {
err.message += ' (' + name + ')';
throw err;
}
finished = true;
} finally {
if (dirp) {
j.closedir(dirp);
}
}
return callback ? finished : items;
};
/**
* Create a directory
*
* @param {string} pathname Path of directory
* @param {number} [mode=0755] Creation mode
* @returns {0}
* @throws {SysError}
*/
fs.mkdir = function (pathname, mode) {
if (mode === undefined) {
mode = 0755;
}
try {
return j.mkdir(pathname, mode);
} catch (err) {
err.message += ' (' + pathname + ')';
throw err;
}
};
/**
* Create a directory and all parents that are necessary
*
* @param {string} pathname Path of directory
* @param {number} [mode=0755] Creation mode of new directories
* @returns {0}
* @throws {SysError}
*/
fs.mkdirp = function (pathname, mode) {
var dirname = '';
var initialIndex = 1;
if (!pathname.startsWith('/')) {
dirname = '.';
initialIndex = 0;
}
const parts = pathname.split('/');
for (var i = initialIndex; i < parts.length; i++) {
dirname += '/' + parts[i];
if (!fs.exists(dirname)) {
fs.mkdir(dirname, mode);
} else if (!fs.is_directory(dirname)) {
errno.fail(errno.ENOTDIR);
}
}
return 0;
};
/**
* Create a FIFO at a given path
*
* @param {string} pathname Path of FIFO
* @param {number} [mode=0644] Creation mode of FIFO
* @returns {0}
* @throws {SysError}
*/
fs.mkfifo = function (pathname, mode) {
if (mode === undefined) {
mode = 0644;
}
return j.mkfifo(pathname, mode);
};
/**
* Normalize a path resolving any `.` or `..` inside and making sure it is an
* absolute path.
*
* Note that this function does not return the canonical path (resolving
* symbolic links), just an absolute path in normalized form.
*
* The path does NOT need to exist for the function to work (as opposed to
* {@link module:fs.realpath}.
*
* @param {string} path The path to normalize
* @returns {string} An absolute path without any `.` or `..` inside
*/
fs.normalize_path = function (path) {
if (path[0] !== '/') {
path = fs.realpath('.') + '/' + path;
}
const nparts = [];
path.split('/')
.filter(function (part) {
return part !== '.';
})
.forEach(function (part) {
if (part === '..') {
nparts.pop();
} else {
nparts.push(part);
}
});
return nparts.join('/');
};
/**
* Get the path referenced by a symbolic link
*
* @param {string} path Path to symbolic link
*
* @param {boolean} [dereference=true]
* Whether to dereference link if it is relative
*
* @returns {string} The target path
* @throws {SysError}
*/
fs.read_link = function (path, dereference) {
if (dereference === undefined) {
dereference = true;
}
// We need to use a buffer because readlink does not add the trailing \0
const buf = new Uint8Array(384);
const length = j.readlink(path, buf, buf.length);
buf = buf.subarray(0, length);
const val = decoder.decode(buf);
if (val[0] === '/' || !dereference) {
return val;
}
const dir = fs.dirname(path);
return (dir === '/' ? '' : dir) + '/' + val;
};
/**
* Read the contents of a file as an UTF-8 string.
*
* @param {string} path Path of file to read
* @returns {string} The contents of the file as a string
* @throws {SysError}
* @see {module:io.read_string}
*/
fs.read_file = function (path) {
const fd = io.open(path, 'r');
try {
return io.read_string(fd);
} finally {
io.close(fd);
}
};
/**
* Get the canonical form of a path (resolving `.`, `..`, and symbolic links)
*
* @example
* // Get the absolute path of the current working directory
* const cwd = fs.realpath('.');
*
* @param {string} path
* @returns {string} The canonical path
* @throws {SysError} If anything goes wrong or path does not exist
*/
fs.realpath = function (path) {
return j.realpath(path);
};
/**
* Move a file or directory
*
* @param {string} oldpath Path to rename
* @param {string} newpath New path of renamed file/dir
* @returns {0}
* @throws {SysError}
*/
fs.rename = function (oldpath, newpath) {
return j.rename(oldpath, newpath);
};
/**
* Delete a directory
*
* @param {string} path Path of directory
* @param {boolean} [recursive=false] Delete even if not empty
* @returns {0}
* @throws {SysError}
*/
fs.rmdir = function (path, recursive) {
if (recursive === undefined) {
recursive = false;
}
if (recursive && !fs.exists(path)) {
return 0;
}
if (recursive) {
fs.list_dir(path, function (item) {
const item_path = path + '/' + item;
if (fs.is_directory(item_path)) {
fs.rmdir(item_path, true);
} else {
fs.unlink(item_path);
}
});
}
return j.rmdir(path);
};
/**
* Obtain information of a file node
*
* @param {string} pathname Path of file node
* @returns {StatBuf} Information on file node
* @throws {SysError}
*/
fs.stat = function (pathname) {
const statbuf = j.lstat(pathname).statbuf;
return {
gid: statbuf.st_gid,
mode: statbuf.st_mode,
size: statbuf.st_size,
time: {
access: statbuf.st_atim.tv_sec,
creation: statbuf.st_ctim.tv_sec,
modification: statbuf.st_mtim.tv_sec,
},
uid: statbuf.st_uid,
};
};
/**
* Create a symbolic link at path2 pointing to path1
*
* @param {string} path1 Symlink target path
* @param {string} path2 Symlink file path
* @returns {0}
* @throws {SysError}
*/
fs.symlink = function (path1, path2) {
return j.symlink(path1, path2);
};
/**
* Delete a file node
*
* @param {string} pathname Path of file node
*
* @param {boolean} [fail_if_not_found=true]
* Pass `false` to ignore ENOENT errors
*
* @returns {0}
* @throws {SysError}
*/
fs.unlink = function (pathname, fail_if_not_found) {
if (fail_if_not_found === undefined) {
fail_if_not_found = true;
}
try {
return j.unlink(pathname);
} catch (err) {
if (!fail_if_not_found && err.errno === errno.ENOENT) {
return 0;
}
err.message += ' (' + pathname + ')';
throw err;
}
};
/**
* Write a string in UTF-8 format to a file
*
* @param {string} path Path to file
* @param {string} contents Contents of file
* @param {number} [mode=0644] Creation mode if file needs to be created
* @returns {number} The number of bytes written
* @throws {SysError}
* @see {@link module:io.write_string}
*/
fs.write_file = function (path, contents, mode) {
if (mode === undefined) {
mode = 0644;
}
var fd;
try {
fd = io.truncate(path, mode, 'w');
return io.write_string(fd, contents);
} catch (err) {
err.message += ' (' + path + ')';
throw err;
} finally {
if (fd) {
io.close(fd);
}
}
};
/**
* Check if a file matches a mode bit given current process' effective gid and
* uid.
*
* @param {string} pathname
* @returns {boolean}
* @throws {SysError} If anything goes wrong or the path does not exist
* @private
*/
function matches_mode(pathname, modebit) {
const umask = modebit;
const gmask = modebit << 3;
const omask = modebit << 6;
try {
const stat = fs.stat(pathname);
if (stat.mode & umask) {
return true;
}
if (stat.mode & gmask && proc.getegid() === stat.gid) {
return true;
}
if (stat.mode & omask && proc.geteuid() === stat.uid) {
return true;
}
} catch (err) {
if (
[
undefined,
errno.EFAULT,
errno.ENAMETOOLONG,
errno.ENOMEM,
errno.EOVERFLOW,
errno.EBADFD,
errno.EINVAL,
].includes(err.errno)
) {
throw err;
} else {
// ignore
}
}
return false;
}
return fs;