io/index.js

const errno = require('errno');

const decoder = new TextDecoder();
const encoder = new TextEncoder();

/**
 * A poll() descriptor
 *
 * @typedef {object} Pollfd
 *
 * @property {number} fd File descriptor to check
 *
 * @property {number} events
 * Events to check (the "See" section list possible values)
 *
 * @property {number} revents
 * Returned events (the "See" section list possible values)
 *
 * @see {@link module:io.POLLIN}
 * @see {@link module:io.POLLPRI}
 * @see {@link module:io.POLLOUT}
 * @see {@link module:io.POLLERR}
 * @see {@link module:io.POLLHUP}
 * @see {@link module:io.POLLNVAL}
 * @see {@link module:io.POLLRDNORM}
 * @see {@link module:io.POLLRDBAND}
 * @see {@link module:io.POLLWRNORM}
 * @see {@link module:io.POLLWRBAND}
 * @see {@link module:io.POLLMSG}
 * @see {@link module:io.POLLREMOVE}
 * @see {@link module:io.POLLRDHUP}
 */

const LF_CODE = '\n'.charCodeAt(0);

/**
 * @exports io
 * @readonly
 * @enum {number}
 */
const io = {
	/* Poll flags */
	POLLIN: 0x1,
	POLLPRI: 0x2,
	POLLOUT: 0x4,
	POLLERR: 0x8,
	POLLHUP: 0x10,
	POLLNVAL: 0x20,
	POLLRDNORM: 0x40,
	POLLRDBAND: 0x80,
	POLLWRNORM: 0x100,
	POLLWRBAND: 0x200,
	POLLMSG: 0x400,
	POLLREMOVE: 0x1000,
	POLLRDHUP: 0x2000,

	/* seek flags */

	/** Seek from start of file */
	SEEK_SET: 0,
	/** Seek from current position */
	SEEK_CUR: 1,
	/** Seek from end of file */
	SEEK_END: 2,
};

const O_APPEND = 02000;
const O_CREAT = 0100;
const O_RDONLY = 0;
const O_RDWR = 2;
const O_TRUNC = 01000;
const O_WRONLY = 1;

const ACCESS_FLAG = {
	r: O_RDONLY,
	w: O_WRONLY,
	rw: O_RDWR,
};

/**
 * Open a file for appending. If the file does not exist it is created. If it
 * exists its contents remain untouched.
 *
 * Note that the file pointer will always be set to EOF before any write
 * operation.
 *
 * Note that the file is always open in write only mode.
 *
 * @param {string} pathname Path to file
 *
 * @param {number} [mode=0644]
 * File mode bits of file in case it needs to be created
 *
 * @returns {number} The file descriptor
 * @throws {SysError}
 * @see {module:io.create}
 * @see {module:io.open}
 * @see {module:io.truncate}
 */
io.append = function (pathname, mode) {
	mode = Number(mode || 0644);

	return j.open(pathname, O_CREAT | O_APPEND | O_WRONLY, mode);
};

/**
 * Close an open file
 *
 * @param {number} fd The file descriptor
 * @param {boolean} [fail_if_closed=true] Fail if file is already closed
 * @returns {0}
 * @throws {SysError}
 */
io.close = function (fd, fail_if_closed) {
	try {
		return j.close(Number(fd));
	} catch (err) {
		if (fail_if_closed || err.errno !== errno.EBADF) {
			throw err;
		}

		return 0;
	}
};

/**
 * Open a file. If the file does not exist it is created. If it exists its
 * contents remain untouched.
 *
 * @param {string} pathname Path to file
 *
 * @param {number} [mode=0644]
 * File mode bits of file in case it needs to be created
 *
 * @param {string} [access='rw']
 * Access mode of file ('r', 'w', or 'rw')
 *
 * @returns {number} The file descriptor
 * @throws {SysError}
 * @see {module:io.append}
 * @see {module:io.open}
 * @see {module:io.truncate}
 */
io.create = function (pathname, mode, access) {
	if (mode === undefined && access === undefined) {
		mode = 0644;
		access = 'rw';
	} else if (access === undefined && typeof mode === 'string') {
		access = mode;
		mode = 0644;
	} else if (access === undefined) {
		access = 'rw';
	}

	mode = Number(mode);
	access = access.toString();

	return j.open(pathname, O_CREAT | ACCESS_FLAG[access], mode);
};

/**
 * Duplicate an open file descriptor
 *
 * @param {number} fd The existing file descriptor
 * @returns {number} The new file descriptor
 * @throws {SysError}
 */
io.dup = function (fd) {
	return j.dup(Number(fd));
};

/**
 * Duplicate an open file descriptor assigning it to another custom fd
 *
 * @param {number} openFd The existing file descriptor
 * @param {number} changedFd The file descriptor that will be changed
 * @returns {number} The changed file descriptor
 * @throws {SysError}
 */
io.dup2 = function (openFd, changedFd) {
	return j.dup2(Number(openFd), Number(changedFd));
};

/**
 * Open an existing file.
 *
 * @param {string} pathname Path to file
 * @param {string} [access='rw'] Access mode ('r', 'w', or 'rw')
 *
 * @returns {number} The file descriptor
 * @throws {SysError}
 * @see {module:io.append}
 * @see {module:io.create}
 * @see {module:io.truncate}
 */
io.open = function (pathname, access) {
	if (access === undefined) {
		access = 'rw';
	}

	try {
		return j.open(pathname, ACCESS_FLAG[access], 0);
	} catch (err) {
		err.message += ' (' + pathname + ')';
		throw err;
	}
};

/**
 * Create a pipe
 *
 * @returns {number[]}
 * An array with two file descriptors where `[0]` item is the read end of the
 * pipe and `[1]` is the write end.
 *
 * @throws {SysError}
 */
io.pipe = function () {
	const fildes = [-1, -1];

	return j.pipe(fildes).fildes;
};

/**
 * The poll() function provides applications with a mechanism for multiplexing
 * input/output over a set of file descriptors.
 *
 * For each member of the array pointed to by `fds`, poll() shall examine the
 * given file descriptor for the event(s) specified in events.
 *
 * The poll() function shall identify those file descriptors on which an
 * application can read or write data, or on which certain events have occurred.
 *
 * @param {Pollfd} fds File descriptors and events to check
 * @param {number} [timeout=-1] Timeout in milliseconds or -1 to wait forever
 * @returns {number} Count of fds with events or 0 on timeout
 * @throws {SysError}
 */
io.poll = function (fds, timeout) {
	if (timeout === undefined) {
		timeout = -1;
	}

	timeout = Number(timeout);

	const result = j.poll(fds, fds.length, timeout);

	fds.length = 0;
	for (var i = 0; i < result.fds.length; i++) {
		fds.push(result.fds[i]);
	}

	return result.value;
};

/**
 * Read bytes from an open file
 *
 * @param {number} fd An open file desriptor
 * @param {Uint8Array} buf Buffer to fill with read bytes
 *
 * @param {number} [count=-1]
 * Maximum number of bytes to read, or `-1` to read until buffer is full or EOF.
 *
 * Note that it is not the same `-1` as `buf.length`, because `buf.length` does
 * not guarantee that the whole buffer is read.
 *
 * @returns {number}
 * The number of bytes read (with 0 meaning end of file).
 *
 * In the case where `count = -1`, a return less than buffer length means that
 * EOF was reached.
 *
 * @throws {SysError}
 */
io.read = function (fd, buf, count) {
	if (count === undefined) {
		count = -1;
	}

	fd = Number(fd);
	count = Number(count);

	if (count === -1) {
		count = buf.length;

		const bleft = buf.length;
		var chunk_buf = buf;

		while (true) {
			const bread = j.read(fd, chunk_buf, bleft);

			if (bread === 0) {
				break;
			}

			if (buf !== chunk_buf) {
				buf.set(chunk_buf.subarray(0, bread), buf.length - bleft);
			}

			bleft -= bread;

			if (bleft === 0) {
				break;
			}

			if (buf === chunk_buf) {
				chunk_buf = new Uint8Array(bleft);
			}
		}

		return count - bleft;
	} else {
		return j.read(fd, buf, count);
	}
};

/**
 * Read contents of a fd until it is exhausted and return them as an Uint8Array.
 *
 * @param {number} fd An open file desriptor
 * @returns {Uint8Array} The bytes contained by the file
 * @throws {SysError}
 */
io.read_fully = function (fd) {
	const buf = new Uint8Array(4096);

	var bytes = new Uint8Array(buf.length);
	var count = 0;

	while (true) {
		const bread = io.read(fd, buf);

		if (bytes.length < count + bread) {
			const new_bytes =
				count > 0x100000
					? new Uint8Array(count + 0x100000)
					: new Uint8Array(count * 2);

			new_bytes.set(bytes);

			bytes = new_bytes;
		}

		bytes.set(buf, count);

		count += bread;

		if (bread < buf.length) {
			break;
		}
	}

	if (bytes.length !== count) {
		const new_bytes = new Uint8Array(count);

		new_bytes.set(bytes.subarray(0, count));

		bytes = new_bytes;
	}

	return bytes;
};

/**
 * Read contents of a fd until it is exhausted and return them as a string.
 *
 * The bytes are interpreted as UTF-8.
 *
 * @param {number} fd An open file desriptor
 * @returns {string} The contents of the file interpreted as an UTF-8 string
 * @throws {SysError}
 */
io.read_string = function (fd) {
	return decoder.decode(io.read_fully(fd));
};

/**
 * Set the pointer of a file descriptor to a given value
 *
 * @param {number} fd An open file descriptor
 * @param {number} offset Relative offset for file pointer
 *
 * @param {number} whence
 * Base of offset. One of the values: {@link module:io.SEEK_SET},
 * {@link module:io.SEEK_CUR}, or {@link module:io.SEEK_END}.
 *
 * @returns {number} The resulting offset measured from start of file
 * @throws {SysError}
 */
io.seek = function (fd, offset, whence) {
	return j.lseek(fd, offset, whence);
};

/**
 * Retrieve the current offset of the file pointer from the start of the file.
 *
 * @param {number} fd An open file desriptor
 * @returns {number} The offset of the file pointer
 * @throws {SysError}
 */
io.tell = function (fd) {
	return io.seek(fd, 0, io.SEEK_CUR);
};

/**
 * Open a file and empty it. If the file does not exist it is created.
 *
 * @param {string} pathname Path to file
 *
 * @param {number} [mode=0644]
 * File mode bits of file in case it needs to be created
 *
 * @param {string} [access='rw']
 * Access mode of file ('r', 'w', or 'rw')
 *
 * @returns {number} The file descriptor
 * @throws {SysError}
 * @see {module:io.append}
 * @see {module:io.create}
 * @see {module:io.open}
 */
io.truncate = function (pathname, mode, access) {
	if (mode === undefined && access === undefined) {
		mode = 0644;
		access = 'rw';
	} else if (access === undefined && typeof mode === 'string') {
		access = mode;
		mode = 0644;
	} else if (access === undefined) {
		access = 'rw';
	}

	mode = Number(mode);
	access = access.toString();

	return j.open(pathname, O_CREAT | O_TRUNC | ACCESS_FLAG[access], mode);
};

/**
 * Write bytes to an open file
 *
 * @param {number} fd An open file desriptor
 * @param {Uint8Array} buf Buffer containing bytes to write
 *
 * @param {number} [count=-1]
 * Maximum number of bytes to write or -1 to keep writing until all buffer has
 * been written.
 *
 * Note that it is not the same `-1` as `buf.length`, because `buf.length` does
 * not guarantee that the whole buffer is written.
 *
 * @returns {number} The number of bytes written
 * @throws {SysError}
 */
io.write = function (fd, buf, count) {
	if (count === undefined) {
		count = -1;
	}

	fd = Number(fd);
	count = Number(count);

	if (count === -1) {
		count = buf.length;

		var bleft = buf.length;

		while (true) {
			const bwritten = j.write(fd, buf, bleft);

			bleft -= bwritten;

			if (bleft === 0) {
				break;
			}

			buf = new Uint8Array(buf, bwritten);
		}

		return count;
	} else {
		return j.write(fd, buf, count);
	}
};

/**
 * Write a string as UTF-8 bytes to an open file
 *
 * @param {number} fd An open file desriptor
 * @param {string} str The string to write
 * @returns {number} The number of bytes written
 * @throws {SysError}
 */
io.write_string = function (fd, str) {
	fd = Number(fd);
	str = str.toString();

	return io.write(fd, encoder.encode(str.toString()));
};

return io;