/*
* Copyright (C) 2020-2021 Jo-Philipp Wich <jo@mein.io>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/**
* # Filesystem Access
*
* The `fs` module provides functions for interacting with the file system.
*
* Functions can be individually imported and directly accessed using the
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#named_import named import}
* syntax:
*
* ```
* import { readlink, popen } from 'fs';
*
* let dest = readlink('/sys/class/net/eth0');
* let proc = popen('ps ww');
* ```
*
* Alternatively, the module namespace can be imported
* using a wildcard import statement:
*
* ```
* import * as fs from 'fs';
*
* let dest = fs.readlink('/sys/class/net/eth0');
* let proc = fs.popen('ps ww');
* ```
*
* Additionally, the filesystem module namespace may also be imported by invoking
* the `ucode` interpreter with the `-lfs` switch.
*
* @module fs
*/
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <dirent.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/file.h>
#include <grp.h>
#include <pwd.h>
#include <glob.h>
#include <fnmatch.h>
#include <limits.h>
#include <fcntl.h>
#if defined(__linux__)
#include <sys/ioctl.h>
#endif
#include "ucode/module.h"
#include "ucode/platform.h"
#define err_return(err) do { last_error = err; return NULL; } while(0)
//static const uc_ops *ops;
static uc_resource_type_t *file_type, *proc_type, *dir_type;
static int last_error = 0;
/**
* Query error information.
*
* Returns a string containing a description of the last occurred error or
* `null` if there is no error information.
*
* @function module:fs#error
*
*
* @returns {?string}
*
* @example
* // Trigger file system error
* unlink('/path/does/not/exist');
*
* // Print error (should yield "No such file or directory")
* print(error(), "\n");
*/
static uc_value_t *
uc_fs_error(uc_vm_t *vm, size_t nargs)
{
uc_value_t *errmsg;
if (last_error == 0)
return NULL;
errmsg = ucv_string_new(strerror(last_error));
last_error = 0;
return errmsg;
}
static uc_value_t *
uc_fs_read_common(uc_vm_t *vm, size_t nargs, const char *type)
{
uc_value_t *limit = uc_fn_arg(0);
uc_value_t *rv = NULL;
char buf[128], *p = NULL, *tmp;
size_t rlen, len = 0;
const char *lstr;
int64_t lsize;
ssize_t llen;
FILE **fp = uc_fn_this(type);
if (!fp || !*fp)
err_return(EBADF);
if (ucv_type(limit) == UC_STRING) {
lstr = ucv_string_get(limit);
llen = ucv_string_length(limit);
if (llen == 4 && !strcmp(lstr, "line")) {
llen = getline(&p, &rlen, *fp);
if (llen == -1) {
free(p);
err_return(errno);
}
len = (size_t)llen;
}
else if (llen == 3 && !strcmp(lstr, "all")) {
while (true) {
rlen = fread(buf, 1, sizeof(buf), *fp);
tmp = realloc(p, len + rlen);
if (!tmp) {
free(p);
err_return(ENOMEM);
}
memcpy(tmp + len, buf, rlen);
p = tmp;
len += rlen;
if (rlen == 0)
break;
}
}
else if (llen == 1) {
llen = getdelim(&p, &rlen, *lstr, *fp);
if (llen == -1) {
free(p);
err_return(errno);
}
len = (size_t)llen;
}
else {
return NULL;
}
}
else if (ucv_type(limit) == UC_INTEGER) {
lsize = ucv_int64_get(limit);
if (lsize <= 0)
return NULL;
p = calloc(1, lsize);
if (!p)
err_return(ENOMEM);
len = fread(p, 1, lsize, *fp);
if (ferror(*fp)) {
free(p);
err_return(errno);
}
}
else {
err_return(EINVAL);
}
rv = ucv_string_new_length(p, len);
free(p);
return rv;
}
static uc_value_t *
uc_fs_write_common(uc_vm_t *vm, size_t nargs, const char *type)
{
uc_value_t *data = uc_fn_arg(0);
size_t len, wsize;
char *str;
FILE **fp = uc_fn_this(type);
if (!fp || !*fp)
err_return(EBADF);
if (ucv_type(data) == UC_STRING) {
len = ucv_string_length(data);
wsize = fwrite(ucv_string_get(data), 1, len, *fp);
}
else {
str = ucv_to_jsonstring(vm, data);
len = str ? strlen(str) : 0;
wsize = fwrite(str, 1, len, *fp);
free(str);
}
if (wsize < len && ferror(*fp))
err_return(errno);
return ucv_int64_new(wsize);
}
static uc_value_t *
uc_fs_flush_common(uc_vm_t *vm, size_t nargs, const char *type)
{
FILE **fp = uc_fn_this(type);
if (!fp || !*fp)
err_return(EBADF);
if (fflush(*fp) != EOF)
err_return(errno);
return ucv_boolean_new(true);
}
static uc_value_t *
uc_fs_fileno_common(uc_vm_t *vm, size_t nargs, const char *type)
{
int fd;
FILE **fp = uc_fn_this(type);
if (!fp || !*fp)
err_return(EBADF);
fd = fileno(*fp);
if (fd == -1)
err_return(errno);
return ucv_int64_new(fd);
}
/**
* Represents a handle for interacting with a program launched by `popen()`.
*
* @class module:fs.proc
* @hideconstructor
*
* @borrows module:fs#error as module:fs.proc#error
*
* @see {@link module:fs#popen|popen()}
*
* @example
*
* const handle = popen(…);
*
* handle.read(…);
* handle.write(…);
* handle.flush();
*
* handle.fileno();
*
* handle.close();
*
* handle.error();
*/
/**
* Closes the program handle and awaits program termination.
*
* Upon calling `close()` on the handle, the program's input or output stream
* (depending on the open mode) is closed. Afterwards, the function awaits the
* termination of the underlying program and returns its exit code.
*
* - When the program was terminated by a signal, the return value will be the
* negative signal number, e.g. `-9` for SIGKILL.
*
* - When the program terminated normally, the return value will be the positive
* exit code of the program.
*
* Returns a negative signal number if the program was terminated by a signal.
*
* Returns a positive exit code if the program terminated normally.
*
* Returns `null` if an error occurred.
*
* @function module:fs.proc#close
*
* @returns {?number}
*/
static uc_value_t *
uc_fs_pclose(uc_vm_t *vm, size_t nargs)
{
FILE **fp = uc_fn_this("fs.proc");
int rc;
if (!fp || !*fp)
err_return(EBADF);
rc = pclose(*fp);
*fp = NULL;
if (rc == -1)
err_return(errno);
if (WIFEXITED(rc))
return ucv_int64_new(WEXITSTATUS(rc));
if (WIFSIGNALED(rc))
return ucv_int64_new(-WTERMSIG(rc));
return ucv_int64_new(0);
}
/**
* Reads a chunk of data from the program handle.
*
* The length argument may be either a positive number of bytes to read, in
* which case the read call returns up to that many bytes, or a string to
* specify a dynamic read size.
*
* - If length is a number, the method will read the specified number of bytes
* from the handle. Reading stops after the given amount of bytes or after
* encountering EOF, whatever comes first.
*
* - If length is the string "line", the method will read an entire line,
* terminated by "\n" (a newline), from the handle. Reading stops at the next
* newline or when encountering EOF. The returned data will contain the
* terminating newline character if one was read.
*
* - If length is the string "all", the method will read from the handle until
* encountering EOF and return the complete contents.
*
* - If length is a single character string, the method will read from the
* handle until encountering the specified character or upon encountering
* EOF. The returned data will contain the terminating character if one was
* read.
*
* Returns a string containing the read data.
*
* Returns an empty string on EOF.
*
* Returns `null` if a read error occurred.
*
* @function module:fs.proc#read
*
* @param {number|string} length
* The length of data to read. Can be a number, the string "line", the string
* "all", or a single character string.
*
* @returns {?string}
*
* @example
* const fp = popen("command", "r");
*
* // Example 1: Read 10 bytes from the handle
* const chunk = fp.read(10);
*
* // Example 2: Read the handle line by line
* for (let line = fp.read("line"); length(line); line = fp.read("line"))
* print(line);
*
* // Example 3: Read the complete contents from the handle
* const content = fp.read("all");
*
* // Example 4: Read until encountering the character ':'
* const field = fp.read(":");
*/
static uc_value_t *
uc_fs_pread(uc_vm_t *vm, size_t nargs)
{
return uc_fs_read_common(vm, nargs, "fs.proc");
}
/**
* Writes a chunk of data to the program handle.
*
* In case the given data is not a string, it is converted to a string before
* being written to the program's stdin. String values are written as-is,
* integer and double values are written in decimal notation, boolean values are
* written as `true` or `false` while arrays and objects are converted to their
* JSON representation before being written. The `null` value is represented by
* an empty string so `proc.write(null)` would be a no-op. Resource values are
* written in the form `<type address>`, e.g. `<fs.file 0x7f60f0981760>`.
*
* If resource, array or object values contain a `tostring()` function in their
* prototypes, then this function is invoked to obtain an alternative string
* representation of the value.
*
* Returns the number of bytes written.
*
* Returns `null` if a write error occurred.
*
* @function module:fs.proc#write
*
* @param {*} data
* The data to be written.
*
* @returns {?number}
*
* @example
* const fp = popen("command", "w");
*
* fp.write("Hello world!\n");
*/
static uc_value_t *
uc_fs_pwrite(uc_vm_t *vm, size_t nargs)
{
return uc_fs_write_common(vm, nargs, "fs.proc");
}
/**
* Forces a write of all buffered data to the underlying handle.
*
* Returns `true` if the data was successfully flushed.
*
* Returns `null` on error.
*
* @function module:fs.proc#flush
*
* @returns {?boolean}
*
*/
static uc_value_t *
uc_fs_pflush(uc_vm_t *vm, size_t nargs)
{
return uc_fs_flush_common(vm, nargs, "fs.proc");
}
/**
* Obtains the number of the handle's underlying file descriptor.
*
* Returns the descriptor number.
*
* Returns `null` on error.
*
* @function module:fs.proc#fileno
*
* @returns {?number}
*/
static uc_value_t *
uc_fs_pfileno(uc_vm_t *vm, size_t nargs)
{
return uc_fs_fileno_common(vm, nargs, "fs.proc");
}
/**
* Starts a process and returns a handle representing the executed process.
*
* The handle will be connected to the process stdin or stdout, depending on the
* value of the mode argument.
*
* The mode argument may be either "r" to open the process for reading (connect
* to its stdin) or "w" to open the process for writing (connect to its stdout).
*
* The mode character "r" or "w" may be optionally followed by "e" to apply the
* FD_CLOEXEC flag onto the open descriptor.
*
* Returns a process handle referring to the executed process.
*
* Returns `null` if an error occurred.
*
* @function module:fs#popen
*
* @param {string} command
* The command to be executed.
*
* @param {string} [mode="r"]
* The open mode of the process handle.
*
* @returns {?module:fs.proc}
*
* @example
* // Open a process
* const process = popen('command', 'r');
*/
static uc_value_t *
uc_fs_popen(uc_vm_t *vm, size_t nargs)
{
uc_value_t *comm = uc_fn_arg(0);
uc_value_t *mode = uc_fn_arg(1);
FILE *fp;
if (ucv_type(comm) != UC_STRING)
err_return(EINVAL);
fp = popen(ucv_string_get(comm),
ucv_type(mode) == UC_STRING ? ucv_string_get(mode) : "r");
if (!fp)
err_return(errno);
return uc_resource_new(proc_type, fp);
}
/**
* Represents a handle for interacting with a file opened by one of the file
* open functions.
*
* @class module:fs.file
* @hideconstructor
*
* @borrows module:fs#error as module:fs.file#error
*
* @see {@link module:fs#open|open()}
* @see {@link module:fs#fdopen|fdopen()}
* @see {@link module:fs#mkstemp|mkstemp()}
* @see {@link module:fs#pipe|pipe()}
*
* @example
*
* const handle = open(…);
*
* handle.read(…);
* handle.write(…);
* handle.flush();
*
* handle.seek(…);
* handle.tell();
*
* handle.isatty();
* handle.fileno();
*
* handle.close();
*
* handle.error();
*/
/**
* Closes the file handle.
*
* Upon calling `close()` on the handle, buffered data is flushed and the
* underlying file descriptor is closed.
*
* Returns `true` if the handle was properly closed.
*
* Returns `null` if an error occurred.
*
* @function module:fs.file#close
*
* @returns {?boolean}
*/
static uc_value_t *
uc_fs_close(uc_vm_t *vm, size_t nargs)
{
FILE **fp = uc_fn_this("fs.file");
if (!fp || !*fp)
err_return(EBADF);
fclose(*fp);
*fp = NULL;
return ucv_boolean_new(true);
}
/**
* Reads a chunk of data from the file handle.
*
* The length argument may be either a positive number of bytes to read, in
* which case the read call returns up to that many bytes, or a string to
* specify a dynamic read size.
*
* - If length is a number, the method will read the specified number of bytes
* from the handle. Reading stops after the given amount of bytes or after
* encountering EOF, whatever comes first.
*
* - If length is the string "line", the method will read an entire line,
* terminated by "\n" (a newline), from the handle. Reading stops at the next
* newline or when encountering EOF. The returned data will contain the
* terminating newline character if one was read.
*
* - If length is the string "all", the method will read from the handle until
* encountering EOF and return the complete contents.
*
* - If length is a single character string, the method will read from the
* handle until encountering the specified character or upon encountering
* EOF. The returned data will contain the terminating character if one was
* read.
*
* Returns a string containing the read data.
*
* Returns an empty string on EOF.
*
* Returns `null` if a read error occurred.
*
* @function module:fs.file#read
*
* @param {number|string} length
* The length of data to read. Can be a number, the string "line", the string
* "all", or a single character string.
*
* @returns {?string}
*
* @example
* const fp = open("file.txt", "r");
*
* // Example 1: Read 10 bytes from the handle
* const chunk = fp.read(10);
*
* // Example 2: Read the handle line by line
* for (let line = fp.read("line"); length(line); line = fp.read("line"))
* print(line);
*
* // Example 3: Read the complete contents from the handle
* const content = fp.read("all");
*
* // Example 4: Read until encountering the character ':'
* const field = fp.read(":");
*/
static uc_value_t *
uc_fs_read(uc_vm_t *vm, size_t nargs)
{
return uc_fs_read_common(vm, nargs, "fs.file");
}
/**
* Writes a chunk of data to the file handle.
*
* In case the given data is not a string, it is converted to a string before
* being written into the file. String values are written as-is, integer and
* double values are written in decimal notation, boolean values are written as
* `true` or `false` while arrays and objects are converted to their JSON
* representation before being written. The `null` value is represented by an
* empty string so `file.write(null)` would be a no-op. Resource values are
* written in the form `<type address>`, e.g. `<fs.file 0x7f60f0981760>`.
*
* If resource, array or object values contain a `tostring()` function in their
* prototypes, then this function is invoked to obtain an alternative string
* representation of the value.
*
* Returns the number of bytes written.
*
* Returns `null` if a write error occurred.
*
* @function module:fs.file#write
*
* @param {*} data
* The data to be written.
*
* @returns {?number}
*
* @example
* const fp = open("file.txt", "w");
*
* fp.write("Hello world!\n");
*/
static uc_value_t *
uc_fs_write(uc_vm_t *vm, size_t nargs)
{
return uc_fs_write_common(vm, nargs, "fs.file");
}
/**
* Set file read position.
*
* Set the read position of the open file handle to the given offset and
* position.
*
* Returns `true` if the read position was set.
*
* Returns `null` if an error occurred.
*
* @function module:fs.file#seek
*
* @param {number} [offset=0]
* The offset in bytes.
*
* @param {number} [position=0]
* The position of the offset.
*
* | Position | Description |
* |----------|----------------------------------------------------------------------------------------------|
* | `0` | The given offset is relative to the start of the file. This is the default value if omitted. |
* | `1` | The given offset is relative to the current read position. |
* | `2` | The given offset is relative to the end of the file. |
*
* @returns {?boolean}
*
* @example
* const fp = open("file.txt", "r");
*
* print(fp.read(100), "\n"); // read 100 bytes...
* fp.seek(0, 0); // ... and reset position to start of file
* print(fp.read(100), "\n"); // ... read same 100 bytes again
*
* fp.seek(10, 1); // skip 10 bytes forward, relative to current offset ...
* fp.tell(); // ... position is at 110 now
*
* fp.seek(-10, 2); // set position to ten bytes before EOF ...
* print(fp.read(100), "\n"); // ... reads 10 bytes at most
*/
static uc_value_t *
uc_fs_seek(uc_vm_t *vm, size_t nargs)
{
uc_value_t *ofs = uc_fn_arg(0);
uc_value_t *how = uc_fn_arg(1);
int whence, res;
off_t offset;
FILE **fp = uc_fn_this("fs.file");
if (!fp || !*fp)
err_return(EBADF);
if (!ofs)
offset = 0;
else if (ucv_type(ofs) != UC_INTEGER)
err_return(EINVAL);
else
offset = (off_t)ucv_int64_get(ofs);
if (!how)
whence = 0;
else if (ucv_type(how) != UC_INTEGER)
err_return(EINVAL);
else
whence = (int)ucv_int64_get(how);
res = fseeko(*fp, offset, whence);
if (res < 0)
err_return(errno);
return ucv_boolean_new(true);
}
/**
* Truncate file to a given size
*
* Returns `true` if the file was successfully truncated.
*
* Returns `null` if an error occurred.
*
* @function module:fs.file#truncate
*
* @param {number} [offset=0]
* The offset in bytes.
*
* @returns {?boolean}
*/
static uc_value_t *
uc_fs_truncate(uc_vm_t *vm, size_t nargs)
{
FILE *fp = uc_fn_thisval("fs.file");
uc_value_t *ofs = uc_fn_arg(0);
off_t offset;
if (!fp)
err_return(EBADF);
if (!ofs)
offset = 0;
else if (ucv_type(ofs) != UC_INTEGER)
err_return(EINVAL);
else
offset = (off_t)ucv_int64_get(ofs);
if (ftruncate(fileno(fp), offset) < 0)
err_return(errno);
return ucv_boolean_new(true);
}
/**
* Locks or unlocks a file.
*
* The mode argument specifies lock/unlock operation flags.
*
* | Flag | Description |
* |---------|------------------------------|
* | "s" | shared lock |
* | "x" | exclusive lock |
* | "n" | don't block when locking |
* | "u" | unlock |
*
* Returns `true` if the file was successfully locked/unlocked.
*
* Returns `null` if an error occurred.
*
* @function module:fs.file#lock
*
* @param {string} [op]
* The lock operation flags
*
* @returns {?boolean}
*/
static uc_value_t *
uc_fs_lock(uc_vm_t *vm, size_t nargs)
{
FILE *fp = uc_fn_thisval("fs.file");
uc_value_t *mode = uc_fn_arg(0);
int i, op = 0;
char *m;
if (!fp)
err_return(EBADF);
if (ucv_type(mode) != UC_STRING)
err_return(EINVAL);
m = ucv_string_get(mode);
for (i = 0; m[i]; i++) {
switch (m[i]) {
case 's': op |= LOCK_SH; break;
case 'x': op |= LOCK_EX; break;
case 'n': op |= LOCK_NB; break;
case 'u': op |= LOCK_UN; break;
default: err_return(EINVAL);
}
}
if (flock(fileno(fp), op) < 0)
err_return(errno);
return ucv_boolean_new(true);
}
/**
* Obtain current read position.
*
* Obtains the current, absolute read position of the open file.
*
* Returns an integer containing the current read offset in bytes.
*
* Returns `null` if an error occurred.
*
* @function module:fs.file#tell
*
* @returns {?number}
*/
static uc_value_t *
uc_fs_tell(uc_vm_t *vm, size_t nargs)
{
off_t offset;
FILE **fp = uc_fn_this("fs.file");
if (!fp || !*fp)
err_return(EBADF);
offset = ftello(*fp);
if (offset < 0)
err_return(errno);
return ucv_int64_new(offset);
}
/**
* Check for TTY.
*
* Checks whether the open file handle refers to a TTY (terminal) device.
*
* Returns `true` if the handle refers to a terminal.
*
* Returns `false` if the handle refers to another kind of file.
*
* Returns `null` on error.
*
* @function module:fs.file#isatty
*
* @returns {?boolean}
*
*/
static uc_value_t *
uc_fs_isatty(uc_vm_t *vm, size_t nargs)
{
FILE **fp = uc_fn_this("fs.file");
int fd;
if (!fp || !*fp)
err_return(EBADF);
fd = fileno(*fp);
if (fd == -1)
err_return(errno);
return ucv_boolean_new(isatty(fd) == 1);
}
/**
* Forces a write of all buffered data to the underlying handle.
*
* Returns `true` if the data was successfully flushed.
*
* Returns `null` on error.
*
* @function module:fs.file#flush
*
* @returns {?boolean}
*
*/
static uc_value_t *
uc_fs_flush(uc_vm_t *vm, size_t nargs)
{
return uc_fs_flush_common(vm, nargs, "fs.file");
}
/**
* Obtains the number of the handle's underlying file descriptor.
*
* Returns the descriptor number.
*
* Returns `null` on error.
*
* @function module:fs.file#fileno
*
* @returns {?number}
*/
static uc_value_t *
uc_fs_fileno(uc_vm_t *vm, size_t nargs)
{
return uc_fs_fileno_common(vm, nargs, "fs.file");
}
#if defined(__linux__)
/**
* Performs an ioctl operation on the file.
*
* The direction parameter specifies who is reading and writing,
* from the user's point of view. It can be one of the following values:
*
* | Direction | Description |
* |-----------|-----------------------------------------------------------------------------------------|
* | 0 | NONE - neither userspace nor kernel is writing, ioctl is executed without passing data. |
* | 1 | WRITE - userspace is writing and kernel is reading. |
* | 2 | READ - kernel is writing and userspace is reading. |
* | 3 | READ+WRITE - userspace is writing and kernel is writing back into the data structure. |
*
* The size parameter has a different purpose depending on the direction parameter:
* - direction = 0 -> the size parameter is not used
* - direction = 1 -> size must be the length (in bytes) of argp
* - direction = 2 -> expected length (in bytes) of the data returned by kernel
* - direction = 3 -> size is the length (in bytes) of argp, and the length of the data returned by kernel.
*
* The argp parameter should be the data to be written for direction '1' and '3', otherwise null.
*
* Returns the result of the ioctl operation; for direction '2' and '3' this is a string containing
* the data, otherwise a number as return code.
* In case of an error, null is returned and the error code is available via last_error.
*
* @function module:fs.file#ioctl
*
* @param {number} direction
* The direction of the ioctl operation.
*
* @param {number} type
* ioctl type (see https://www.kernel.org/doc/html/latest/userspace-api/ioctl/ioctl-number.html)
*
* @param {number} num
* ioctl sequence number.
*
* @param {number} size
* The size of the ioctl operation payload.
*
* @param {?string} payload
* The ioctl payload.
*
* @returns {?number|?string}
*/
static uc_value_t *
uc_fs_ioctl(uc_vm_t *vm, size_t nargs)
{
FILE *fp = uc_fn_thisval("fs.file");
uc_value_t *direction = uc_fn_arg(0);
uc_value_t *type = uc_fn_arg(1);
uc_value_t *num = uc_fn_arg(2);
uc_value_t *size = uc_fn_arg(3);
uc_value_t *payload = uc_fn_arg(4);
char *buf = NULL;
unsigned long req = 0;
unsigned int dir, ty, nr;
size_t sz;
int fd, ret;
bool freebuf = false;
if (!fp)
err_return(EBADF);
fd = fileno(fp);
if (fd == -1)
err_return(EBADF);
if (ucv_type(direction) != UC_INTEGER || ucv_type(type) != UC_INTEGER ||
ucv_type(num) != UC_INTEGER || ucv_type(size) != UC_INTEGER)
err_return(EINVAL);
dir = ucv_uint64_get(direction);
sz = ucv_uint64_get(size);
ty = ucv_uint64_get(type);
nr = ucv_uint64_get(num);
switch (dir) {
case 0: /* ioctl w/o read and write */
req = _IOC(_IOC_NONE, ty, nr, 0);
break;
case 1: /* ioctl write */
if (ucv_type(payload) != UC_STRING)
err_return(EINVAL);
req = _IOC(_IOC_WRITE, ty, nr, sz);
buf = ucv_string_get(payload);
break;
case 2: /* ioctl read */
req = _IOC(_IOC_READ, ty, nr, sz);
buf = xalloc(sz);
if (!buf)
err_return(ENOMEM);
freebuf = true;
break;
case 3: /* ioctl read+write */
req = _IOC((_IOC_READ|_IOC_WRITE), ty, nr, sz);
buf = ucv_string_get(payload);
break;
default: err_return(EINVAL);
}
ret = ioctl(fd, req, buf);
if (ret < 0) {
if (freebuf)
free(buf);
err_return(errno);
}
if (dir >= 2) {
payload = ucv_string_new_length(buf, sz);
if (freebuf)
free(buf);
} else {
payload = ucv_uint64_new(ret);
}
return payload;
}
#endif
/**
* Opens a file.
*
* The mode argument specifies the way the file is opened, it may
* start with one of the following values:
*
* | Mode | Description |
* |---------|---------------------------------------------------------------------------------------------------------------|
* | "r" | Opens a file for reading. The file must exist. |
* | "w" | Opens a file for writing. If the file exists, it is truncated. If the file does not exist, it is created. |
* | "a" | Opens a file for appending. Data is written at the end of the file. If the file does not exist, it is created. |
* | "r+" | Opens a file for both reading and writing. The file must exist. |
* | "w+" | Opens a file for both reading and writing. If the file exists, it is truncated. If the file does not exist, it is created. |
* | "a+" | Opens a file for both reading and appending. Data can be read and written at the end of the file. If the file does not exist, it is created. |
*
* Additionally, the following flag characters may be appended to
* the mode value:
*
* | Flag | Description |
* |---------|---------------------------------------------------------------------------------------------------------------|
* | "x" | Opens a file for exclusive creation. If the file exists, the `open` call fails. |
* | "e" | Opens a file with the `O_CLOEXEC` flag set, ensuring that the file descriptor is closed on `exec` calls. |
*
* If the mode is one of `"w…"` or `"a…"`, the permission argument
* controls the filesystem permissions bits used when creating
* the file.
*
* Returns a file handle object associated with the opened file.
*
* @function module:fs#open
*
* @param {string} path
* The path to the file.
*
* @param {string} [mode="r"]
* The file opening mode.
*
* @param {number} [perm=0o666]
* The file creation permissions (for modes `w…` and `a…`)
*
* @returns {?module:fs.file}
*
* @example
* // Open a file in read-only mode
* const fileHandle = open('file.txt', 'r');
*/
static uc_value_t *
uc_fs_open(uc_vm_t *vm, size_t nargs)
{
int open_mode, open_flags, fd, i;
uc_value_t *path = uc_fn_arg(0);
uc_value_t *mode = uc_fn_arg(1);
uc_value_t *perm = uc_fn_arg(2);
mode_t open_perm = 0666;
FILE *fp;
char *m;
if (ucv_type(path) != UC_STRING)
err_return(EINVAL);
m = (ucv_type(mode) == UC_STRING) ? ucv_string_get(mode) : "r";
switch (*m) {
case 'r':
open_mode = O_RDONLY;
open_flags = 0;
break;
case 'w':
open_mode = O_WRONLY;
open_flags = O_CREAT | O_TRUNC;
break;
case 'a':
open_mode = O_WRONLY;
open_flags = O_CREAT | O_APPEND;
break;
default:
err_return(EINVAL);
}
for (i = 1; m[i]; i++) {
switch (m[i]) {
case '+': open_mode = O_RDWR; break;
case 'x': open_flags |= O_EXCL; break;
case 'e': open_flags |= O_CLOEXEC; break;
}
}
if (perm) {
if (ucv_type(perm) != UC_INTEGER)
err_return(EINVAL);
open_perm = ucv_int64_get(perm);
}
#ifdef O_LARGEFILE
open_flags |= open_mode | O_LARGEFILE;
#else
open_flags |= open_mode;
#endif
fd = open(ucv_string_get(path), open_flags, open_perm);
if (fd < 0)
return NULL;
fp = fdopen(fd, m);
if (!fp) {
i = errno;
close(fd);
err_return(i);
}
return uc_resource_new(file_type, fp);
}
/**
* Associates a file descriptor number with a file handle object.
*
* The mode argument controls how the file handle object is opened
* and must match the open mode of the underlying descriptor.
*
* It may be set to one of the following values:
*
* | Mode | Description |
* |---------|--------------------------------------------------------------------------------------------------------------|
* | "r" | Opens a file stream for reading. The file descriptor must be valid and opened in read mode. |
* | "w" | Opens a file stream for writing. The file descriptor must be valid and opened in write mode. |
* | "a" | Opens a file stream for appending. The file descriptor must be valid and opened in write mode. |
* | "r+" | Opens a file stream for both reading and writing. The file descriptor must be valid and opened in read/write mode. |
* | "w+" | Opens a file stream for both reading and writing. The file descriptor must be valid and opened in read/write mode. |
* | "a+" | Opens a file stream for both reading and appending. The file descriptor must be valid and opened in read/write mode. |
*
* Returns the file handle object associated with the file descriptor.
*
* @function module:fs#fdopen
*
* @param {number} fd
* The file descriptor.
*
* @param {string} [mode="r"]
* The open mode.
*
* @returns {Object}
*
* @example
* // Associate file descriptors of stdin and stdout with handles
* const stdinHandle = fdopen(0, 'r');
* const stdoutHandle = fdopen(1, 'w');
*/
static uc_value_t *
uc_fs_fdopen(uc_vm_t *vm, size_t nargs)
{
uc_value_t *fdno = uc_fn_arg(0);
uc_value_t *mode = uc_fn_arg(1);
int64_t n;
FILE *fp;
if (ucv_type(fdno) != UC_INTEGER)
err_return(EINVAL);
n = ucv_int64_get(fdno);
if (n < 0 || n > INT_MAX)
err_return(EBADF);
fp = fdopen((int)n,
ucv_type(mode) == UC_STRING ? ucv_string_get(mode) : "r");
if (!fp)
err_return(errno);
return uc_resource_new(file_type, fp);
}
/**
* Represents a handle for interacting with a directory opened by `opendir()`.
*
* @class module:fs.dir
* @hideconstructor
*
* @borrows module:fs#error as module:fs.dir#error
*
* @see {@link module:fs#opendir|opendir()}
*
* @example
*
* const handle = opendir(…);
*
* handle.read();
*
* handle.tell();
* handle.seek(…);
*
* handle.close();
*
* handle.error();
*/
/**
* Read the next entry from the open directory.
*
* Returns a string containing the entry name.
*
* Returns `null` if there are no more entries to read.
*
* Returns `null` if an error occurred.
*
* @function module:fs.dir#read
*
* @returns {?string}
*/
static uc_value_t *
uc_fs_readdir(uc_vm_t *vm, size_t nargs)
{
DIR **dp = uc_fn_this("fs.dir");
struct dirent *e;
if (!dp || !*dp)
err_return(EINVAL);
errno = 0;
e = readdir(*dp);
if (!e)
err_return(errno);
return ucv_string_new(e->d_name);
}
/**
* Obtain current read position.
*
* Returns the current read position in the open directory handle which can be
* passed back to the `seek()` function to return to this position. This is
* mainly useful to read an open directory handle (or specific items) multiple
* times.
*
* Returns an integer referring to the current position.
*
* Returns `null` if an error occurred.
*
* @function module:fs.dir#tell
*
* @returns {?number}
*/
static uc_value_t *
uc_fs_telldir(uc_vm_t *vm, size_t nargs)
{
DIR **dp = uc_fn_this("fs.dir");
long position;
if (!dp || !*dp)
err_return(EBADF);
position = telldir(*dp);
if (position == -1)
err_return(errno);
return ucv_int64_new((int64_t)position);
}
/**
* Set read position.
*
* Sets the read position within the open directory handle to the given offset
* value. The offset value should be obtained by a previous call to `tell()` as
* the specific integer values are implementation defined.
*
* Returns `true` if the read position was set.
*
* Returns `null` if an error occurred.
*
* @function module:fs.dir#seek
*
* @param {number} offset
* Position value obtained by `tell()`.
*
* @returns {?boolean}
*
* @example
*
* const handle = opendir("/tmp");
* const begin = handle.tell();
*
* print(handle.read(), "\n");
*
* handle.seek(begin);
*
* print(handle.read(), "\n"); // prints the first entry again
*/
static uc_value_t *
uc_fs_seekdir(uc_vm_t *vm, size_t nargs)
{
uc_value_t *ofs = uc_fn_arg(0);
DIR **dp = uc_fn_this("fs.dir");
long position;
if (ucv_type(ofs) != UC_INTEGER)
err_return(EINVAL);
if (!dp || !*dp)
err_return(EBADF);
position = (long)ucv_int64_get(ofs);
seekdir(*dp, position);
return ucv_boolean_new(true);
}
/**
* Closes the directory handle.
*
* Closes the underlying file descriptor referring to the opened directory.
*
* Returns `true` if the handle was properly closed.
*
* Returns `null` if an error occurred.
*
* @function module:fs.dir#close
*
* @returns {?boolean}
*/
static uc_value_t *
uc_fs_closedir(uc_vm_t *vm, size_t nargs)
{
DIR **dp = uc_fn_this("fs.dir");
if (!dp || !*dp)
err_return(EBADF);
closedir(*dp);
*dp = NULL;
return ucv_boolean_new(true);
}
/**
* Opens a directory and returns a directory handle associated with the open
* directory descriptor.
*
* Returns a director handle referring to the open directory.
*
* Returns `null` if an error occurred.
*
* @function module:fs#opendir
*
* @param {string} path
* The path to the directory.
*
* @returns {?module:fs.dir}
*
* @example
* // Open a directory
* const directory = opendir('path/to/directory');
*/
static uc_value_t *
uc_fs_opendir(uc_vm_t *vm, size_t nargs)
{
uc_value_t *path = uc_fn_arg(0);
DIR *dp;
if (ucv_type(path) != UC_STRING)
err_return(EINVAL);
dp = opendir(ucv_string_get(path));
if (!dp)
err_return(errno);
return uc_resource_new(dir_type, dp);
}
/**
* Reads the target path of a symbolic link.
*
* Returns a string containing the target path.
*
* Returns `null` if an error occurred.
*
* @function module:fs#readlink
*
* @param {string} path
* The path to the symbolic link.
*
* @returns {?string}
*
* @example
* // Read the value of a symbolic link
* const targetPath = readlink('symbolicLink');
*/
static uc_value_t *
uc_fs_readlink(uc_vm_t *vm, size_t nargs)
{
uc_value_t *path = uc_fn_arg(0);
uc_value_t *res;
ssize_t buflen = 0, rv;
char *buf = NULL, *tmp;
if (ucv_type(path) != UC_STRING)
err_return(EINVAL);
do {
buflen += 128;
tmp = realloc(buf, buflen);
if (!tmp) {
free(buf);
err_return(ENOMEM);
}
buf = tmp;
rv = readlink(ucv_string_get(path), buf, buflen);
if (rv == -1) {
free(buf);
err_return(errno);
}
if (rv < buflen)
break;
}
while (true);
res = ucv_string_new_length(buf, rv);
free(buf);
return res;
}
/**
* @typedef {Object} module:fs.FileStatResult
* @property {Object} dev - The device information.
* @property {number} dev.major - The major device number.
* @property {number} dev.minor - The minor device number.
* @property {Object} perm - The file permissions.
* @property {boolean} perm.setuid - Whether the setuid bit is set.
* @property {boolean} perm.setgid - Whether the setgid bit is set.
* @property {boolean} perm.sticky - Whether the sticky bit is set.
* @property {boolean} perm.user_read - Whether the file is readable by the owner.
* @property {boolean} perm.user_write - Whether the file is writable by the owner.
* @property {boolean} perm.user_exec - Whether the file is executable by the owner.
* @property {boolean} perm.group_read - Whether the file is readable by the group.
* @property {boolean} perm.group_write - Whether the file is writable by the group.
* @property {boolean} perm.group_exec - Whether the file is executable by the group.
* @property {boolean} perm.other_read - Whether the file is readable by others.
* @property {boolean} perm.other_write - Whether the file is writable by others.
* @property {boolean} perm.other_exec - Whether the file is executable by others.
* @property {number} inode - The inode number.
* @property {number} mode - The file mode.
* @property {number} nlink - The number of hard links.
* @property {number} uid - The user ID of the owner.
* @property {number} gid - The group ID of the owner.
* @property {number} size - The file size in bytes.
* @property {number} blksize - The block size for file system I/O.
* @property {number} blocks - The number of 512-byte blocks allocated for the file.
* @property {number} atime - The timestamp when the file was last accessed.
* @property {number} mtime - The timestamp when the file was last modified.
* @property {number} ctime - The timestamp when the file status was last changed.
* @property {string} type - The type of the file ("directory", "file", etc.).
*/
static uc_value_t *
uc_fs_stat_common(uc_vm_t *vm, size_t nargs, bool use_lstat)
{
uc_value_t *path = uc_fn_arg(0);
uc_value_t *res, *o;
struct stat st;
int rv;
if (ucv_type(path) != UC_STRING)
err_return(EINVAL);
rv = (use_lstat ? lstat : stat)(ucv_string_get(path), &st);
if (rv == -1)
err_return(errno);
res = ucv_object_new(vm);
if (!res)
err_return(ENOMEM);
o = ucv_object_new(vm);
if (o) {
ucv_object_add(o, "major", ucv_int64_new(major(st.st_dev)));
ucv_object_add(o, "minor", ucv_int64_new(minor(st.st_dev)));
ucv_object_add(res, "dev", o);
}
o = ucv_object_new(vm);
if (o) {
ucv_object_add(o, "setuid", ucv_boolean_new(st.st_mode & S_ISUID));
ucv_object_add(o, "setgid", ucv_boolean_new(st.st_mode & S_ISGID));
ucv_object_add(o, "sticky", ucv_boolean_new(st.st_mode & S_ISVTX));
ucv_object_add(o, "user_read", ucv_boolean_new(st.st_mode & S_IRUSR));
ucv_object_add(o, "user_write", ucv_boolean_new(st.st_mode & S_IWUSR));
ucv_object_add(o, "user_exec", ucv_boolean_new(st.st_mode & S_IXUSR));
ucv_object_add(o, "group_read", ucv_boolean_new(st.st_mode & S_IRGRP));
ucv_object_add(o, "group_write", ucv_boolean_new(st.st_mode & S_IWGRP));
ucv_object_add(o, "group_exec", ucv_boolean_new(st.st_mode & S_IXGRP));
ucv_object_add(o, "other_read", ucv_boolean_new(st.st_mode & S_IROTH));
ucv_object_add(o, "other_write", ucv_boolean_new(st.st_mode & S_IWOTH));
ucv_object_add(o, "other_exec", ucv_boolean_new(st.st_mode & S_IXOTH));
ucv_object_add(res, "perm", o);
}
ucv_object_add(res, "inode", ucv_int64_new((int64_t)st.st_ino));
ucv_object_add(res, "mode", ucv_int64_new((int64_t)st.st_mode & ~S_IFMT));
ucv_object_add(res, "nlink", ucv_int64_new((int64_t)st.st_nlink));
ucv_object_add(res, "uid", ucv_int64_new((int64_t)st.st_uid));
ucv_object_add(res, "gid", ucv_int64_new((int64_t)st.st_gid));
ucv_object_add(res, "size", ucv_int64_new((int64_t)st.st_size));
ucv_object_add(res, "blksize", ucv_int64_new((int64_t)st.st_blksize));
ucv_object_add(res, "blocks", ucv_int64_new((int64_t)st.st_blocks));
ucv_object_add(res, "atime", ucv_int64_new((int64_t)st.st_atime));
ucv_object_add(res, "mtime", ucv_int64_new((int64_t)st.st_mtime));
ucv_object_add(res, "ctime", ucv_int64_new((int64_t)st.st_ctime));
if (S_ISREG(st.st_mode))
ucv_object_add(res, "type", ucv_string_new("file"));
else if (S_ISDIR(st.st_mode))
ucv_object_add(res, "type", ucv_string_new("directory"));
else if (S_ISCHR(st.st_mode))
ucv_object_add(res, "type", ucv_string_new("char"));
else if (S_ISBLK(st.st_mode))
ucv_object_add(res, "type", ucv_string_new("block"));
else if (S_ISFIFO(st.st_mode))
ucv_object_add(res, "type", ucv_string_new("fifo"));
else if (S_ISLNK(st.st_mode))
ucv_object_add(res, "type", ucv_string_new("link"));
else if (S_ISSOCK(st.st_mode))
ucv_object_add(res, "type", ucv_string_new("socket"));
else
ucv_object_add(res, "type", ucv_string_new("unknown"));
return res;
}
/**
* Retrieves information about a file or directory.
*
* Returns an object containing information about the file or directory.
*
* Returns `null` if an error occurred, e.g. due to insufficient permissions.
*
* @function module:fs#stat
*
* @param {string} path
* The path to the file or directory.
*
* @returns {?module:fs.FileStatResult}
*
* @example
* // Get information about a file
* const fileInfo = stat('path/to/file');
*/
static uc_value_t *
uc_fs_stat(uc_vm_t *vm, size_t nargs)
{
return uc_fs_stat_common(vm, nargs, false);
}
/**
* Retrieves information about a file or directory, without following symbolic
* links.
*
* Returns an object containing information about the file or directory.
*
* Returns `null` if an error occurred, e.g. due to insufficient permissions.
*
* @function module:fs#lstat
*
* @param {string} path
* The path to the file or directory.
*
* @returns {?module:fs.FileStatResult}
*
* @example
* // Get information about a directory
* const dirInfo = lstat('path/to/directory');
*/
static uc_value_t *
uc_fs_lstat(uc_vm_t *vm, size_t nargs)
{
return uc_fs_stat_common(vm, nargs, true);
}
/**
* Creates a new directory.
*
* Returns `true` if the directory was successfully created.
*
* Returns `null` if an error occurred, e.g. due to inexistent path.
*
* @function module:fs#mkdir
*
* @param {string} path
* The path to the new directory.
*
* @returns {?boolean}
*
* @example
* // Create a directory
* mkdir('path/to/new-directory');
*/
static uc_value_t *
uc_fs_mkdir(uc_vm_t *vm, size_t nargs)
{
uc_value_t *path = uc_fn_arg(0);
uc_value_t *mode = uc_fn_arg(1);
if (ucv_type(path) != UC_STRING ||
(mode && ucv_type(mode) != UC_INTEGER))
err_return(EINVAL);
if (mkdir(ucv_string_get(path), (mode_t)(mode ? ucv_int64_get(mode) : 0777)) == -1)
err_return(errno);
return ucv_boolean_new(true);
}
/**
* Removes the specified directory.
*
* Returns `true` if the directory was successfully removed.
*
* Returns `null` if an error occurred, e.g. due to inexistent path.
*
* @function module:fs#rmdir
*
* @param {string} path
* The path to the directory to be removed.
*
* @returns {?boolean}
*
* @example
* // Remove a directory
* rmdir('path/to/directory');
*/
static uc_value_t *
uc_fs_rmdir(uc_vm_t *vm, size_t nargs)
{
uc_value_t *path = uc_fn_arg(0);
if (ucv_type(path) != UC_STRING)
err_return(EINVAL);
if (rmdir(ucv_string_get(path)) == -1)
err_return(errno);
return ucv_boolean_new(true);
}
/**
* Creates a new symbolic link.
*
* Returns `true` if the symlink was successfully created.
*
* Returns `null` if an error occurred, e.g. due to inexistent path.
*
* @function module:fs#symlink
*
* @param {string} target
* The target of the symbolic link.
*
* @param {string} path
* The path of the symbolic link.
*
* @returns {?boolean}
*
* @example
* // Create a symbolic link
* symlink('target', 'path/to/symlink');
*/
static uc_value_t *
uc_fs_symlink(uc_vm_t *vm, size_t nargs)
{
uc_value_t *dest = uc_fn_arg(0);
uc_value_t *path = uc_fn_arg(1);
if (ucv_type(dest) != UC_STRING ||
ucv_type(path) != UC_STRING)
err_return(EINVAL);
if (symlink(ucv_string_get(dest), ucv_string_get(path)) == -1)
err_return(errno);
return ucv_boolean_new(true);
}
/**
* Removes the specified file or symbolic link.
*
* Returns `true` if the unlink operation was successful.
*
* Returns `null` if an error occurred, e.g. due to inexistent path.
*
* @function module:fs#unlink
*
* @param {string} path
* The path to the file or symbolic link.
*
* @returns {?boolean}
*
* @example
* // Remove a file
* unlink('path/to/file');
*/
static uc_value_t *
uc_fs_unlink(uc_vm_t *vm, size_t nargs)
{
uc_value_t *path = uc_fn_arg(0);
if (ucv_type(path) != UC_STRING)
err_return(EINVAL);
if (unlink(ucv_string_get(path)) == -1)
err_return(errno);
return ucv_boolean_new(true);
}
/**
* Retrieves the current working directory.
*
* Returns a string containing the current working directory path.
*
* Returns `null` if an error occurred.
*
* @function module:fs#getcwd
*
* @returns {?string}
*
* @example
* // Get the current working directory
* const cwd = getcwd();
*/
static uc_value_t *
uc_fs_getcwd(uc_vm_t *vm, size_t nargs)
{
uc_value_t *res;
char *buf = NULL, *tmp;
size_t buflen = 0;
do {
buflen += 128;
tmp = realloc(buf, buflen);
if (!tmp) {
free(buf);
err_return(ENOMEM);
}
buf = tmp;
if (getcwd(buf, buflen) != NULL)
break;
if (errno == ERANGE)
continue;
free(buf);
err_return(errno);
}
while (true);
res = ucv_string_new(buf);
free(buf);
return res;
}
/**
* Changes the current working directory to the specified path.
*
* Returns `true` if the permission change was successful.
*
* Returns `null` if an error occurred, e.g. due to insufficient permissions or
* invalid arguments.
*
* @function module:fs#chdir
*
* @param {string} path
* The path to the new working directory.
*
* @returns {?boolean}
*
* @example
* // Change the current working directory
* chdir('new-directory');
*/
static uc_value_t *
uc_fs_chdir(uc_vm_t *vm, size_t nargs)
{
uc_value_t *path = uc_fn_arg(0);
if (ucv_type(path) != UC_STRING)
err_return(EINVAL);
if (chdir(ucv_string_get(path)) == -1)
err_return(errno);
return ucv_boolean_new(true);
}
/**
* Changes the permission mode bits of a file or directory.
*
* Returns `true` if the permission change was successful.
*
* Returns `null` if an error occurred, e.g. due to insufficient permissions or
* invalid arguments.
*
* @function module:fs#chmod
*
* @param {string} path
* The path to the file or directory.
*
* @param {number} mode
* The new mode (permissions).
*
* @returns {?boolean}
*
* @example
* // Change the mode of a file
* chmod('path/to/file', 0o644);
*/
static uc_value_t *
uc_fs_chmod(uc_vm_t *vm, size_t nargs)
{
uc_value_t *path = uc_fn_arg(0);
uc_value_t *mode = uc_fn_arg(1);
if (ucv_type(path) != UC_STRING ||
ucv_type(mode) != UC_INTEGER)
err_return(EINVAL);
if (chmod(ucv_string_get(path), (mode_t)ucv_int64_get(mode)) == -1)
err_return(errno);
return ucv_boolean_new(true);
}
static bool
uc_fs_resolve_user(uc_value_t *v, uid_t *uid)
{
struct passwd *pw = NULL;
int64_t n;
char *s;
*uid = (uid_t)-1;
switch (ucv_type(v)) {
case UC_INTEGER:
n = ucv_int64_get(v);
if (n < -1) {
errno = ERANGE;
return false;
}
*uid = (uid_t)n;
return true;
case UC_STRING:
s = ucv_string_get(v);
pw = getpwnam(s);
if (!pw) {
errno = ENOENT;
return false;
}
*uid = pw->pw_uid;
return true;
case UC_NULL:
return true;
default:
errno = EINVAL;
return false;
}
}
static bool
uc_fs_resolve_group(uc_value_t *v, gid_t *gid)
{
struct group *gr = NULL;
int64_t n;
char *s;
*gid = (gid_t)-1;
switch (ucv_type(v)) {
case UC_INTEGER:
n = ucv_int64_get(v);
if (n < -1) {
errno = ERANGE;
return false;
}
*gid = (gid_t)n;
return true;
case UC_STRING:
s = ucv_string_get(v);
gr = getgrnam(s);
if (!gr) {
errno = ENOENT;
return false;
}
*gid = gr->gr_gid;
return true;
case UC_NULL:
return true;
default:
errno = EINVAL;
return false;
}
}
/**
* Changes the owner and group of a file or directory.
*
* The user and group may be specified either as uid or gid number respectively,
* or as a string containing the user or group name, in which case it is
* resolved to the proper uid/gid first.
*
* If either the user or group parameter is omitted or given as `-1`,
* it is not changed.
*
* Returns `true` if the ownership change was successful.
*
* Returns `null` if an error occurred or if a user/group name cannot be
* resolved to a uid/gid value.
*
* @function module:fs#chown
*
* @param {string} path
* The path to the file or directory.
*
* @param {number|string} [uid=-1]
* The new owner's user ID. When given as number, it is used as-is, when given
* as string, the user name is resolved to the corresponding uid first.
*
* @param {number|string} [gid=-1]
* The new group's ID. When given as number, it is used as-is, when given as
* string, the group name is resolved to the corresponding gid first.
*
* @returns {?boolean}
*
* @example
* // Change the owner of a file
* chown('path/to/file', 1000);
*
* // Change the group of a directory
* chown('/htdocs/', null, 'www-data');
*/
static uc_value_t *
uc_fs_chown(uc_vm_t *vm, size_t nargs)
{
uc_value_t *path = uc_fn_arg(0);
uc_value_t *user = uc_fn_arg(1);
uc_value_t *group = uc_fn_arg(2);
uid_t uid;
gid_t gid;
if (ucv_type(path) != UC_STRING)
err_return(EINVAL);
if (!uc_fs_resolve_user(user, &uid) ||
!uc_fs_resolve_group(group, &gid))
err_return(errno);
if (chown(ucv_string_get(path), uid, gid) == -1)
err_return(errno);
return ucv_boolean_new(true);
}
/**
* Renames or moves a file or directory.
*
* Returns `true` if the rename operation was successful.
*
* Returns `null` if an error occurred.
*
* @function module:fs#rename
*
* @param {string} oldPath
* The current path of the file or directory.
*
* @param {string} newPath
* The new path of the file or directory.
*
* @returns {?boolean}
*
* @example
* // Rename a file
* rename('old-name.txt', 'new-name.txt');
*/
static uc_value_t *
uc_fs_rename(uc_vm_t *vm, size_t nargs)
{
uc_value_t *oldpath = uc_fn_arg(0);
uc_value_t *newpath = uc_fn_arg(1);
if (ucv_type(oldpath) != UC_STRING ||
ucv_type(newpath) != UC_STRING)
err_return(EINVAL);
if (rename(ucv_string_get(oldpath), ucv_string_get(newpath)))
err_return(errno);
return ucv_boolean_new(true);
}
static uc_value_t *
uc_fs_glob(uc_vm_t *vm, size_t nargs)
{
uc_value_t *pat, *arr;
glob_t gl = { 0 };
size_t i;
for (i = 0; i < nargs; i++) {
pat = uc_fn_arg(i);
if (ucv_type(pat) != UC_STRING) {
globfree(&gl);
err_return(EINVAL);
}
glob(ucv_string_get(pat), i ? GLOB_APPEND : 0, NULL, &gl);
}
arr = ucv_array_new(vm);
for (i = 0; i < gl.gl_pathc; i++)
ucv_array_push(arr, ucv_string_new(gl.gl_pathv[i]));
globfree(&gl);
return arr;
}
/**
* Retrieves the directory name of a path.
*
* Returns the directory name component of the specified path.
*
* Returns `null` if the path argument is not a string.
*
* @function module:fs#dirname
*
* @param {string} path
* The path to extract the directory name from.
*
* @returns {?string}
*
* @example
* // Get the directory name of a path
* const directoryName = dirname('/path/to/file.txt');
*/
static uc_value_t *
uc_fs_dirname(uc_vm_t *vm, size_t nargs)
{
uc_value_t *path = uc_fn_arg(0);
size_t i;
char *s;
if (ucv_type(path) != UC_STRING)
err_return(EINVAL);
i = ucv_string_length(path);
s = ucv_string_get(path);
if (i == 0)
return ucv_string_new(".");
for (i--; s[i] == '/'; i--)
if (i == 0)
return ucv_string_new("/");
for (; s[i] != '/'; i--)
if (i == 0)
return ucv_string_new(".");
for (; s[i] == '/'; i--)
if (i == 0)
return ucv_string_new("/");
return ucv_string_new_length(s, i + 1);
}
/**
* Retrieves the base name of a path.
*
* Returns the base name component of the specified path.
*
* Returns `null` if the path argument is not a string.
*
* @function module:fs#basename
*
* @param {string} path
* The path to extract the base name from.
*
* @returns {?string}
*
* @example
* // Get the base name of a path
* const baseName = basename('/path/to/file.txt');
*/
static uc_value_t *
uc_fs_basename(uc_vm_t *vm, size_t nargs)
{
uc_value_t *path = uc_fn_arg(0);
size_t i, len, skip;
char *s;
if (ucv_type(path) != UC_STRING)
err_return(EINVAL);
len = ucv_string_length(path);
s = ucv_string_get(path);
if (len == 0)
return ucv_string_new(".");
for (i = len - 1, skip = 0; i > 0 && s[i] == '/'; i--, skip++)
;
for (; i > 0 && s[i - 1] != '/'; i--)
;
return ucv_string_new_length(s + i, len - i - skip);
}
static int
uc_fs_lsdir_sort_fn(const void *k1, const void *k2)
{
uc_value_t * const *v1 = k1;
uc_value_t * const *v2 = k2;
return strcmp(ucv_string_get(*v1), ucv_string_get(*v2));
}
/**
* Lists the content of a directory.
*
* Returns a sorted array of the names of files and directories in the specified
* directory.
*
* Returns `null` if an error occurred, e.g. if the specified directory cannot
* be opened.
*
* @function module:fs#lsdir
*
* @param {string} path
* The path to the directory.
*
* @returns {?string[]}
*
* @example
* // List the content of a directory
* const fileList = lsdir('/path/to/directory');
*/
static uc_value_t *
uc_fs_lsdir(uc_vm_t *vm, size_t nargs)
{
uc_value_t *path = uc_fn_arg(0);
uc_value_t *pat = uc_fn_arg(1);
uc_value_t *res = NULL;
uc_regexp_t *reg;
struct dirent *e;
DIR *d;
if (ucv_type(path) != UC_STRING)
err_return(EINVAL);
switch (ucv_type(pat)) {
case UC_NULL:
case UC_STRING:
case UC_REGEXP:
break;
default:
err_return(EINVAL);
}
d = opendir(ucv_string_get(path));
if (!d)
err_return(errno);
res = ucv_array_new(vm);
while ((e = readdir(d)) != NULL) {
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, ".."))
continue;
if (ucv_type(pat) == UC_REGEXP) {
reg = (uc_regexp_t *)pat;
if (regexec(®->regexp, e->d_name, 0, NULL, 0) == REG_NOMATCH)
continue;
}
else if (ucv_type(pat) == UC_STRING) {
if (fnmatch(ucv_string_get(pat), e->d_name, 0) == FNM_NOMATCH)
continue;
}
ucv_array_push(res, ucv_string_new(e->d_name));
}
closedir(d);
ucv_array_sort(res, uc_fs_lsdir_sort_fn);
return res;
}
/**
* Creates a unique, ephemeral temporary file.
*
* Creates a new temporary file, opens it in read and write mode, unlinks it and
* returns a file handle object referring to the yet open but deleted file.
*
* Upon closing the handle, the associated file will automatically vanish from
* the system.
*
* The optional path template argument may be used to override the path and name
* chosen for the temporary file. If the path template contains no path element,
* `/tmp/` is prepended, if it does not end with `XXXXXX`, then * `.XXXXXX` is
* appended to it. The `XXXXXX` sequence is replaced with a random value
* ensuring uniqueness of the temporary file name.
*
* Returns a file handle object referring to the ephemeral file on success.
*
* Returns `null` if an error occurred, e.g. on insufficient permissions or
* inaccessible directory.
*
* @function module:fs#mkstemp
*
* @param {string} [template="/tmp/XXXXXX"]
* The path template to use when forming the temporary file name.
*
* @returns {?module:fs.file}
*
* @example
* // Create a unique temporary file in the current working directory
* const tempFile = mkstemp('./data-XXXXXX');
*/
static uc_value_t *
uc_fs_mkstemp(uc_vm_t *vm, size_t nargs)
{
uc_value_t *template = uc_fn_arg(0);
bool ends_with_template = false;
char *path, *t;
FILE *fp;
size_t l;
int fd;
if (template && ucv_type(template) != UC_STRING)
err_return(EINVAL);
t = ucv_string_get(template);
l = ucv_string_length(template);
ends_with_template = (l >= 6 && strcmp(&t[l - 6], "XXXXXX") == 0);
if (t && strchr(t, '/')) {
if (ends_with_template)
xasprintf(&path, "%s", t);
else
xasprintf(&path, "%s.XXXXXX", t);
}
else if (t) {
if (ends_with_template)
xasprintf(&path, "/tmp/%s", t);
else
xasprintf(&path, "/tmp/%s.XXXXXX", t);
}
else {
xasprintf(&path, "/tmp/XXXXXX");
}
do {
fd = mkstemp(path);
}
while (fd == -1 && errno == EINTR);
if (fd == -1) {
free(path);
err_return(errno);
}
unlink(path);
free(path);
fp = fdopen(fd, "r+");
if (!fp) {
close(fd);
err_return(errno);
}
return uc_resource_new(file_type, fp);
}
/**
* Checks the accessibility of a file or directory.
*
* The optional modes argument specifies the access modes which should be
* checked. A file is only considered accessible if all access modes specified
* in the modes argument are possible.
*
* The following modes are recognized:
*
* | Mode | Description |
* |------|---------------------------------------|
* | "r" | Tests whether the file is readable. |
* | "w" | Tests whether the file is writable. |
* | "x" | Tests whether the file is executable. |
* | "f" | Tests whether the file exists. |
*
* Returns `true` if the given path is accessible or `false` when it is not.
*
* Returns `null` if an error occurred, e.g. due to inaccessible intermediate
* path components, invalid path arguments etc.
*
* @function module:fs#access
*
* @param {string} path
* The path to the file or directory.
*
* @param {number} [mode="f"]
* Optional access mode.
*
* @returns {?boolean}
*
* @example
* // Check file read and write accessibility
* const isAccessible = access('path/to/file', 'rw');
*
* // Check execute permissions
* const mayExecute = access('/usr/bin/example', 'x');
*/
static uc_value_t *
uc_fs_access(uc_vm_t *vm, size_t nargs)
{
uc_value_t *path = uc_fn_arg(0);
uc_value_t *test = uc_fn_arg(1);
int mode = F_OK;
char *p;
if (ucv_type(path) != UC_STRING)
err_return(EINVAL);
if (test && ucv_type(test) != UC_STRING)
err_return(EINVAL);
for (p = ucv_string_get(test); p && *p; p++) {
switch (*p) {
case 'r':
mode |= R_OK;
break;
case 'w':
mode |= W_OK;
break;
case 'x':
mode |= X_OK;
break;
case 'f':
mode |= F_OK;
break;
default:
err_return(EINVAL);
}
}
if (access(ucv_string_get(path), mode) == -1)
err_return(errno);
return ucv_boolean_new(true);
}
/**
* Reads the content of a file, optionally limited to the given amount of bytes.
*
* Returns a string containing the file contents.
*
* Returns `null` if an error occurred, e.g. due to insufficient permissions.
*
* @function module:fs#readfile
*
* @param {string} path
* The path to the file.
*
* @param {number} [limit]
* Number of bytes to limit the result to. When omitted, the entire content is
* returned.
*
* @returns {?string}
*
* @example
* // Read first 100 bytes of content
* const content = readfile('path/to/file', 100);
*
* // Read entire file content
* const content = readfile('path/to/file');
*/
static uc_value_t *
uc_fs_readfile(uc_vm_t *vm, size_t nargs)
{
uc_value_t *path = uc_fn_arg(0);
uc_value_t *size = uc_fn_arg(1);
uc_value_t *res = NULL;
uc_stringbuf_t *buf;
ssize_t limit = -1;
size_t rlen, blen;
FILE *fp;
if (ucv_type(path) != UC_STRING)
err_return(EINVAL);
if (size) {
if (ucv_type(size) != UC_INTEGER)
err_return(EINVAL);
limit = ucv_int64_get(size);
}
fp = fopen(ucv_string_get(path), "r");
if (!fp)
err_return(errno);
buf = ucv_stringbuf_new();
if (limit > -1 && limit < BUFSIZ)
setvbuf(fp, NULL, _IONBF, 0);
while (limit != 0) {
blen = 1024;
if (limit > 0 && blen > (size_t)limit)
blen = (size_t)limit;
printbuf_memset(buf, printbuf_length(buf) + blen - 1, 0, 1);
buf->bpos -= blen;
rlen = fread(buf->buf + buf->bpos, 1, blen, fp);
buf->bpos += rlen;
if (rlen < blen)
break;
if (limit > 0)
limit -= rlen;
}
if (ferror(fp)) {
fclose(fp);
printbuf_free(buf);
err_return(errno);
}
fclose(fp);
/* add sentinel null byte but don't count it towards the string length */
printbuf_memappend_fast(buf, "\0", 1);
res = ucv_stringbuf_finish(buf);
((uc_string_t *)res)->length--;
return res;
}
/**
* Writes the given data to a file, optionally truncated to the given amount
* of bytes.
*
* In case the given data is not a string, it is converted to a string before
* being written into the file. String values are written as-is, integer and
* double values are written in decimal notation, boolean values are written as
* `true` or `false` while arrays and objects are converted to their JSON
* representation before being written into the file. The `null` value is
* represented by an empty string so `writefile(…, null)` would write an empty
* file. Resource values are written in the form `<type address>`, e.g.
* `<fs.file 0x7f60f0981760>`.
*
* If resource, array or object values contain a `tostring()` function in their
* prototypes, then this function is invoked to obtain an alternative string
* representation of the value.
*
* If a file already exists at the given path, it is truncated. If no file
* exists, it is created with default permissions 0o666 masked by the currently
* effective umask.
*
* Returns the number of bytes written.
*
* Returns `null` if an error occurred, e.g. due to insufficient permissions.
*
* @function module:fs#writefile
*
* @param {string} path
* The path to the file.
*
* @param {*} data
* The data to be written.
*
* @param {number} [limit]
* Truncates the amount of data to be written to the specified amount of bytes.
* When omitted, the entire content is written.
*
* @returns {?number}
*
* @example
* // Write string to a file
* const bytesWritten = writefile('path/to/file', 'Hello, World!');
*
* // Write object as JSON to a file and limit to 1024 bytes at most
* const obj = { foo: "Hello world", bar: true, baz: 123 };
* const bytesWritten = writefile('debug.txt', obj, 1024);
*/
static uc_value_t *
uc_fs_writefile(uc_vm_t *vm, size_t nargs)
{
uc_value_t *path = uc_fn_arg(0);
uc_value_t *data = uc_fn_arg(1);
uc_value_t *size = uc_fn_arg(2);
uc_stringbuf_t *buf = NULL;
ssize_t limit = -1;
size_t wlen = 0;
int err = 0;
FILE *fp;
if (ucv_type(path) != UC_STRING)
err_return(EINVAL);
if (size) {
if (ucv_type(size) != UC_INTEGER)
err_return(EINVAL);
limit = ucv_int64_get(size);
}
fp = fopen(ucv_string_get(path), "w");
if (!fp)
err_return(errno);
if (data && ucv_type(data) != UC_STRING) {
buf = xprintbuf_new();
ucv_to_stringbuf_formatted(vm, buf, data, 0, '\0', 0);
if (limit < 0 || limit > printbuf_length(buf))
limit = printbuf_length(buf);
wlen = fwrite(buf->buf, 1, limit, fp);
if (wlen < (size_t)limit)
err = errno;
printbuf_free(buf);
}
else if (data) {
if (limit < 0 || (size_t)limit > ucv_string_length(data))
limit = ucv_string_length(data);
wlen = fwrite(ucv_string_get(data), 1, limit, fp);
if (wlen < (size_t)limit)
err = errno;
}
fclose(fp);
if (err)
err_return(err);
return ucv_uint64_new(wlen);
}
/**
* Resolves the absolute path of a file or directory.
*
* Returns a string containing the resolved path.
*
* Returns `null` if an error occurred, e.g. due to insufficient permissions.
*
* @function module:fs#realpath
*
* @param {string} path
* The path to the file or directory.
*
* @returns {?string}
*
* @example
* // Resolve the absolute path of a file
* const absolutePath = realpath('path/to/file', 'utf8');
*/
static uc_value_t *
uc_fs_realpath(uc_vm_t *vm, size_t nargs)
{
uc_value_t *path = uc_fn_arg(0), *rv;
char *resolved;
if (ucv_type(path) != UC_STRING)
err_return(EINVAL);
resolved = realpath(ucv_string_get(path), NULL);
if (!resolved)
err_return(errno);
rv = ucv_string_new(resolved);
free(resolved);
return rv;
}
/**
* Creates a pipe and returns file handle objects associated with the read- and
* write end of the pipe respectively.
*
* Returns a two element array containing both a file handle object open in read
* mode referring to the read end of the pipe and a file handle object open in
* write mode referring to the write end of the pipe.
*
* Returns `null` if an error occurred.
*
* @function module:fs#pipe
*
* @returns {?module:fs.file[]}
*
* @example
* // Create a pipe
* const pipeHandles = pipe();
* pipeHandles[1].write("Hello world\n");
* print(pipeHandles[0].read("line"));
*/
static uc_value_t *
uc_fs_pipe(uc_vm_t *vm, size_t nargs)
{
int pfds[2], err;
FILE *rfp, *wfp;
uc_value_t *rv;
if (pipe(pfds) == -1)
err_return(errno);
rfp = fdopen(pfds[0], "r");
if (!rfp) {
err = errno;
close(pfds[0]);
close(pfds[1]);
err_return(err);
}
wfp = fdopen(pfds[1], "w");
if (!wfp) {
err = errno;
fclose(rfp);
close(pfds[1]);
err_return(err);
}
rv = ucv_array_new_length(vm, 2);
ucv_array_push(rv, uc_resource_new(file_type, rfp));
ucv_array_push(rv, uc_resource_new(file_type, wfp));
return rv;
}
static const uc_function_list_t proc_fns[] = {
{ "read", uc_fs_pread },
{ "write", uc_fs_pwrite },
{ "close", uc_fs_pclose },
{ "flush", uc_fs_pflush },
{ "fileno", uc_fs_pfileno },
{ "error", uc_fs_error },
};
static const uc_function_list_t file_fns[] = {
{ "read", uc_fs_read },
{ "write", uc_fs_write },
{ "seek", uc_fs_seek },
{ "tell", uc_fs_tell },
{ "close", uc_fs_close },
{ "flush", uc_fs_flush },
{ "fileno", uc_fs_fileno },
{ "error", uc_fs_error },
{ "isatty", uc_fs_isatty },
{ "truncate", uc_fs_truncate },
{ "lock", uc_fs_lock },
#if defined(__linux__)
{ "ioctl", uc_fs_ioctl },
#endif
};
static const uc_function_list_t dir_fns[] = {
{ "read", uc_fs_readdir },
{ "seek", uc_fs_seekdir },
{ "tell", uc_fs_telldir },
{ "close", uc_fs_closedir },
{ "error", uc_fs_error },
};
static const uc_function_list_t global_fns[] = {
{ "error", uc_fs_error },
{ "open", uc_fs_open },
{ "fdopen", uc_fs_fdopen },
{ "opendir", uc_fs_opendir },
{ "popen", uc_fs_popen },
{ "readlink", uc_fs_readlink },
{ "stat", uc_fs_stat },
{ "lstat", uc_fs_lstat },
{ "mkdir", uc_fs_mkdir },
{ "rmdir", uc_fs_rmdir },
{ "symlink", uc_fs_symlink },
{ "unlink", uc_fs_unlink },
{ "getcwd", uc_fs_getcwd },
{ "chdir", uc_fs_chdir },
{ "chmod", uc_fs_chmod },
{ "chown", uc_fs_chown },
{ "rename", uc_fs_rename },
{ "glob", uc_fs_glob },
{ "dirname", uc_fs_dirname },
{ "basename", uc_fs_basename },
{ "lsdir", uc_fs_lsdir },
{ "mkstemp", uc_fs_mkstemp },
{ "access", uc_fs_access },
{ "readfile", uc_fs_readfile },
{ "writefile", uc_fs_writefile },
{ "realpath", uc_fs_realpath },
{ "pipe", uc_fs_pipe },
};
static void close_proc(void *ud)
{
FILE *fp = ud;
if (fp)
pclose(fp);
}
static void close_file(void *ud)
{
FILE *fp = ud;
int n;
n = fp ? fileno(fp) : -1;
if (n > 2)
fclose(fp);
}
static void close_dir(void *ud)
{
DIR *dp = ud;
if (dp)
closedir(dp);
}
void uc_module_init(uc_vm_t *vm, uc_value_t *scope)
{
uc_function_list_register(scope, global_fns);
proc_type = uc_type_declare(vm, "fs.proc", proc_fns, close_proc);
file_type = uc_type_declare(vm, "fs.file", file_fns, close_file);
dir_type = uc_type_declare(vm, "fs.dir", dir_fns, close_dir);
ucv_object_add(scope, "stdin", uc_resource_new(file_type, stdin));
ucv_object_add(scope, "stdout", uc_resource_new(file_type, stdout));
ucv_object_add(scope, "stderr", uc_resource_new(file_type, stderr));
}