callisto/lfs.c
Jeremy Baxter 6590351e48 Add fs.mkdir and fs.rmdir functions, improve lfs.c
- Add fs.mkdir
- Add fs.rmdir
- Change library description
- Re-order #includes
- Use #defines for error messages
2023-08-01 13:35:02 +12:00

624 lines
15 KiB
C

/***
* Files, directories and
* file system manipulation.
* @module fs
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fts.h>
#include <libgen.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef BSD
# include <string.h>
#endif
#include <unistd.h>
#include <lua.h>
#include <lauxlib.h>
#include "callisto.h"
#include "util.h"
/* Error messages */
#define E_PATHNAMETOOLONG "pathname too long"
#define E_WORKDIRNOTVALID "working directory is no longer valid"
#define E_COMPNOTEXIST "component of path does not exist"
#define E_DIRNOTEMPTY "directory not empty"
#define E_MOUNTPOINT "directory is busy"
#define E_NOSUCHDEST "no such file or directory"
#define E_FTRAVERSE "failed to traverse directory"
#define E_NOSUCHDIR "no such directory"
#define E_STICKYDIR "permission denied (sticky directory)"
#define E_INTFAULT "internal error (EFAULT)"
#define E_MVPARENT "cannot move a parent directory of pathname"
#define E_FEXISTS "file exists"
#define E_MAXLINK "maximum link count reached"
#define E_NOSPACE "insufficient space left on file system"
#define E_NOTADIR "pathname or a component of pathname is not a directory"
#define E_SYMLINK "could not translate pathname; too many symbolic links"
#define E_DIFFFS "pathnames are on different file systems"
#define E_DIRDOT "last component of path is '.'"
#define E_ISADIR "cannot move a file to the name of a directory"
#define E_IOERR "I/O error"
#define E_NOMEM "insufficent memory"
#define E_QUOTA "file system quota reached"
#define E_PERM "permission denied"
#define E_ROFS "read-only file system"
/***
* Returns the last component of the given pathname,
* removing any trailing '/' characters. If the given
* path consists entirely of '/' characters, the string
* `"/"` is returned. If *path* is an empty string,
* the string `"."` is returned.
*
* This function may return nil if the given
* pathname exceeds the system's path length
* limit (On most Linux systems this will be 4096).
*
* @function basename
* @usage fs.basename(arg[0])
* @tparam string path The path to process.
*/
static int
fs_basename(lua_State *L)
{
const char *ret;
char *path; /* parameter 1 (string) */
path = strndup(luaL_checkstring(L, 1), lua_rawlen(L, 1));
ret = basename(path);
if (ret == NULL && errno == ENAMETOOLONG) /* check if path is too long */
return lfail(L, E_PATHNAMETOOLONG);
lua_pushstring(L, ret);
return 1;
}
/***
* Returns the parent directory of the pathname
* given. Any trailing '/' characters are not
* counted as part of the directory name.
* If the given path is an empty string or contains
* no '/' characters, the string `"."` is returned,
* signifying the current directory.
*
* This function may return nil if the given
* pathname exceeds the system's path length
* limit (On most Linux systems this will be 4096).
*
* @function dirname
* @usage fs.dirname(arg[0])
* @tparam string path The path to process.
*/
static int
fs_dirname(lua_State *L)
{
const char *ret;
char *path; /* parameter 1 (string) */
path = strndup(luaL_checkstring(L, 1), lua_rawlen(L, 1));
ret = dirname(path);
if (ret == NULL && errno == ENAMETOOLONG) /* check if path is too long */
return lfail(L, E_PATHNAMETOOLONG);
lua_pushstring(L, ret);
return 1;
}
/***
* Returns true if the given pathname exists
* in the file system, or returns false if it
* does not.
*
* This function may throw an error if the given
* pathname exceeds the system's path length
* limit (On most Linux systems this will be 4096).
*
* @function exists
* @usage
if fs.exists("/etc/fstab") then
-- ...
end
* @tparam string path The path of the file to look for.
*/
static int
fs_exists(lua_State *L)
{
const char *path;
int ret;
path = luaL_checkstring(L, 1);
ret = access(path, F_OK); /* check if file exists */
if (ret == -1 && errno == ENAMETOOLONG) /* check if path is too long */
return lfail(L, E_PATHNAMETOOLONG);
lua_pushboolean(L, ret == 0);
return 1;
}
/*
* Taken from OpenBSD mkdir(1)
* mkpath -- create directories.
* path - path
* mode - file mode of terminal directory
* dir_mode - file mode of intermediate directories
*/
static int
mkpath(char *path, mode_t mode, mode_t dir_mode)
{
struct stat sb;
char *slash;
int done;
slash = path;
for (;;) {
slash += strspn(slash, "/");
slash += strcspn(slash, "/");
done = (*slash == '\0');
*slash = '\0';
if (mkdir(path, done ? mode : dir_mode) == 0) {
if (mode > 0777 && chmod(path, mode) == -1)
return -1;
} else {
int mkdir_errno = errno;
if (stat(path, &sb) == -1) {
/* Not there; use mkdir()s errno */
errno = mkdir_errno;
return -1;
}
if (!S_ISDIR(sb.st_mode)) {
/* Is there, but isn't a directory */
errno = ENOTDIR;
return -1;
}
}
if (done)
break;
*slash = '/';
}
return 0;
}
/***
* Creates a new directory.
*
* If *recursive* is true, creates intermediate directories
* as required; behaves as POSIX `mkdir -p` would.
*
* On success, returns true. Otherwise returns nil plus
* an error message.
*
* This function will return nil plus an error message
* if one of the following conditions are met:
*
* - A component of one of the given pathnames is not a directory
*
* - The given pathname exceeds the system's path length limit
*
* - A component of the path prefix does not exist
*
* - Search permission is denied for a component of the path prefix,
* or write permission is denied on the parent directory of the
* directory to be created.
*
* - The given pathname could not be translated as a result
* of too many symbolic links
*
* - The directory to be created resides on a read-only file system
*
* - The pathname given exists as a file
*
* - There is no space left on the file system
*
* - The current user's quota of disk blocks on the file system
* containing the parent directory of the directory to be created
* has been exhausted
*
* @function mkdir
* @usage fs.mkdir("/usr/local/bin")
* @tparam string dir The path of the directory to create.
* @tparam[opt] boolean recursive Whether to create intermediate
* directories as required.
*/
static int
fs_mkdir(lua_State *L)
{
char *dir; /* parameter 1 (string) */
int recursive; /* parameter 2 (boolean) */
int ret;
dir = (char *)luaL_checkstring(L, 1);
if (!lua_isboolean(L, 2) && !lua_isnoneornil(L, 2)) {
luaL_typeerror(L, 2, "boolean");
}
recursive = lua_toboolean(L, 2);
if (recursive) {
ret = mkpath(dir, 0777, 0777);
} else {
ret = mkdir(dir, 0777);
}
if (ret == 0) {
lua_pushboolean(L, 1);
return 1;
}
switch (errno) {
case ENOTDIR:
return lfail(L, E_NOTADIR);
break;
case ENAMETOOLONG:
return lfail(L, E_PATHNAMETOOLONG);
break;
case ENOENT:
return lfail(L, E_COMPNOTEXIST);
break;
case EACCES:
return lfail(L, E_PERM);
break;
case ELOOP:
return lfail(L, E_SYMLINK);
break;
case EROFS:
return lfail(L, E_ROFS);
break;
case EEXIST:
return lfail(L, E_FEXISTS);
break;
case ENOSPC:
return lfail(L, E_NOSPACE);
break;
case EDQUOT:
return lfail(L, E_QUOTA);
break;
case EIO:
return lfail(L, E_IOERR);
break;
case EFAULT:
return lfail(L, E_INTFAULT);
break;
}
return 0;
}
/***
* Moves the file at the path *src* to the path *dest*,
* moving it between directories if required. If *dest*
* exists, it is first removed. Both *src* and *dest*
* must be of the same type (that is, both directories or
* both non-directories) and must reside on the same file system.
*
* This function will return nil plus an error message
* if one of the following conditions are met:
*
* - One of the given pathnames exceeds the system's path length limit
*
* - One of the given pathnames point to a file that does not exist
*
* - An attempt was made to move a parent directory of a pathname
*
* - The current user is denied permission to perform the action
*
* - One of the given pathnames could not be translated as a result
* of too many symbolic links
*
* - A component of one of the given pathnames is not a directory
*
* - *src* is a directory, but *dest* is not
*
* - *dest* is a directory, but *src* is not
*
* - The given pathnames are on different file systems
*
* - There is no space left on the file system
*
* - The current user's quota of disk blocks on the file system
* containing the directory of *dest* has been exhausted
*
* - The directory containing *dest* resides on a read-only file system
*
* @function move
* @usage fs.move("file1", "file2")
* @tparam string src The pathname of the file to move.
* @tparam string dest The destination pathname.
*/
static int
fs_move(lua_State *L)
{
int ret;
const char *src; /* parameter 1 (string) */
const char *dest; /* parameter 2 (string) */
src = luaL_checkstring(L, 1);
dest = luaL_checkstring(L, 2);
ret = rename(src, dest); /* move file */
if (ret == 0) { /* check for success */
lua_pushboolean(L, 1);
return 1;
}
switch (errno) {
case ENAMETOOLONG:
return lfail(L, E_PATHNAMETOOLONG);
break;
case ENOENT:
return lfail(L, E_NOSUCHDEST);
break;
case EACCES:
return lfail(L, E_PERM);
break;
case EPERM:
return lfail(L, E_STICKYDIR);
break;
case ELOOP:
return lfail(L, E_SYMLINK);
break;
case EMLINK:
return lfail(L, E_MAXLINK);
break;
case ENOTDIR:
return lfail(L, E_NOTADIR);
break;
case EISDIR:
return lfail(L, E_ISADIR);
break;
case EXDEV:
return lfail(L, E_DIFFFS);
break;
case ENOSPC:
return lfail(L, E_NOSPACE);
break;
case EDQUOT:
return lfail(L, E_QUOTA);
break;
case EIO:
return lfail(L, E_IOERR);
break;
case EROFS:
return lfail(L, E_ROFS);
break;
case EFAULT:
return lfail(L, E_INTFAULT);
break;
case EINVAL:
return lfail(L, E_MVPARENT);
break;
}
return 0;
}
/***
* Removes an empty directory.
*
* On success, returns true. Otherwise returns nil plus
* an error message.
*
* This function will return nil plus an error message
* if one of the following conditions are met:
*
* - A component of the given path is not a directory
*
* - The given pathname exceeds the system's path length limit
*
* - The named directory does not exist
*
* - The given pathname could not be translated as a result
* of too many symbolic links
*
* - The named directory contains files other than '.' and '..' in it
*
* - Search permission is denied for a component of the path prefix,
* or write permission is denied on the directory containing the directory
* to be removed
*
* - The directory containing the directory to be removed is marked sticky,
* and neither the containing directory nor the directory to be removed are
* owned by the current user
*
* - The directory to be removed or the directory containing it has its
* immutable or append-only flag set
*
* - The directory to be removed is the mount point for a mounted file system
*
* - The last component of the directory to be removed is a '.'
*
* - The directory to be created resides on a read-only file system
*
* @function mkdir
* @usage fs.rmdir("yourdirectory")
* @tparam string dir The path of the directory to remove.
*/
static int
fs_rmdir(lua_State *L)
{
const char *dir;
int ret;
dir = luaL_checkstring(L, 1);
ret = rmdir(dir);
if (ret == 0) {
lua_pushboolean(L, 1);
return 1;
}
switch (errno) {
case ENOTDIR:
return lfail(L, E_NOTADIR);
break;
case ENAMETOOLONG:
return lfail(L, E_PATHNAMETOOLONG);
break;
case ENOENT:
return lfail(L, E_NOSUCHDIR);
break;
case ELOOP:
return lfail(L, E_SYMLINK);
break;
case ENOTEMPTY:
return lfail(L, E_DIRNOTEMPTY);
break;
case EACCES:
return lfail(L, E_PERM);
break;
case EBUSY:
return lfail(L, E_MOUNTPOINT);
break;
case EINVAL:
return lfail(L, E_DIRDOT);
break;
case EIO:
return lfail(L, E_IOERR);
break;
case EROFS:
return lfail(L, E_ROFS);
break;
case EFAULT:
return lfail(L, E_INTFAULT);
break;
}
return 0;
}
/***
* Returns or sets the current working directory.
*
* If *dir* is nil, returns the current working
* directory. Otherwise, sets the current working
* directory to *dir* and returns true on success.
*
* This function will return nil and an error
* message if one of the following conditions are met:
*
* - One of the given pathnames exceeds the system's path length limit
*
* - The current user is denied permission to perform the action
*
* - The current working directory or *dir* no longer exists
*
* - *dir* is not nil, nor is it the name of a directory
*
* - *dir* could not be translated as a result of too many symbolic links
*
* @function workdir
* @usage
-- Store the current working directory in a variable:
local wd = fs.workdir()
-- Set the current working directory to the value of
-- the HOME environment variable:
fs.workdir(environ["HOME"])
* @tparam[opt] string dir The directory to set as the current working directory.
*/
static int
fs_workdir(lua_State *L)
{
const char *workdir; /* parameter 1 (string) */
char *buffer; /* buffer used by getcwd() */
char *ret;
if (lua_isnoneornil(L, 1)) { /* if first argument was not given... */
buffer = malloc(sizeof(char *) * 512);
ret = getcwd(buffer, 512);
if (ret != NULL) {
lua_pushstring(L, buffer);
free(buffer);
return 1;
}
free(buffer);
switch (errno) {
case EACCES:
return lfail(L, E_PERM);
break;
case EFAULT:
return lfail(L, E_INTFAULT);
break;
case ENOENT:
return lfail(L, E_WORKDIRNOTVALID);
break;
case ENOMEM:
return lfail(L, E_NOMEM);
break;
case ERANGE:
case ENAMETOOLONG: /* glibc */
return lfail(L, E_PATHNAMETOOLONG);
break;
}
return 0;
} else {
workdir = luaL_checkstring(L, 1);
if (chdir(workdir) == 0) {
lua_pushboolean(L, 1);
return 1;
}
switch (errno) {
case ENOTDIR:
return lfail(L, E_NOTADIR);
break;
case ENAMETOOLONG:
return lfail(L, E_PATHNAMETOOLONG);
break;
case ENOENT:
return lfail(L, E_NOSUCHDEST);
break;
case ELOOP:
return lfail(L, E_SYMLINK);
break;
case EACCES:
return lfail(L, E_PERM);
break;
case EFAULT:
return lfail(L, E_INTFAULT);
break;
case EIO:
return lfail(L, E_IOERR);
break;
}
return 0;
}
}
static const luaL_Reg fslib[] = {
{"basename", fs_basename},
{"dirname", fs_dirname},
{"exists", fs_exists},
{"mkdir", fs_mkdir},
{"move", fs_move},
{"rmdir", fs_rmdir},
{"workdir", fs_workdir},
{NULL, NULL}
};
int
luaopen_fs(lua_State *L)
{
luaL_newlib(L, fslib);
return 1;
}