123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059 | // A lang for filesystem paths
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <ftw.h>
#include <gc.h>
#include <glob.h>
#include <grp.h>
#include <pwd.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "../unistr-fixed.h"
#include "c_strings.h"
#include "enums.h"
#include "integers.h"
#include "lists.h"
#include "optionals.h"
#include "paths.h"
#include "print.h"
#include "structs.h"
#include "text.h"
#include "types.h"
#include "util.h"
static const Path_t HOME_PATH = (Path_t){"~"}, ROOT_PATH = (Path_t){"/"}, CURDIR_PATH = (Path_t){"."},
PARENT_PATH = (Path_t){".."};
typedef enum { PATH_ABSOLUTE, PATH_RELATIVE, PATH_HOME } pathtype_t;
static pathtype_t path_type(Path_t path) {
if (!path) return PATH_ABSOLUTE;
if (path[0] == '/') return PATH_ABSOLUTE;
if (path[0] == '~' && (path[1] == '\0' || path[1] == '/')) return PATH_HOME;
return PATH_RELATIVE;
}
static void normalize_inplace(char path[PATH_MAX]) {
if (path[0] == '.' && path[1 + strspn(path + 1, "/")] == '\0') {
path[1] = '\0';
return;
}
static char buf[PATH_MAX];
char *src = path, *dest = buf;
// Leading "/"
if (*src == '/') {
*(dest++) = *(src++);
}
*dest = '\0';
for (size_t component_len; *src != '\0' && dest < &buf[PATH_MAX - 1]; src += component_len + 1) {
component_len = strcspn(src, "/");
if (component_len == 0) {
; // Skip empty "//"s:
} else if (component_len == 1 && src[0] == '.' && dest > buf) {
; // Skip "." components
} else {
// Add "/" if there's a previous non-slash
if (dest > buf && dest[-1] != '/') {
*(dest++) = '/';
*dest = '\0';
}
// For ".."
if (component_len == 2 && src[0] == '.' && src[1] == '.') {
// Find previous component:
char *prev_slash = dest - 2;
while (prev_slash >= buf && *prev_slash != '/')
prev_slash -= 1;
// If previous component is not "..", then pop it
if (prev_slash > buf && *prev_slash == '/'
&& strncmp(prev_slash, "/../", (size_t)(dest - prev_slash)) != 0) {
dest = prev_slash;
} else {
// Otherwise we need to keep the ".."
*(dest++) = '.';
*(dest++) = '.';
}
} else {
// Otherwise copy over the component
memcpy(dest, src, component_len);
dest += component_len;
}
*dest = '\0';
}
if (src[component_len] == '\0') break;
}
*(dest++) = '\0';
if (dest == buf) {
path[0] = '.';
path[1] = '\0';
} else {
memcpy(path, buf, strlen(buf) + 1);
}
}
char *path_from_buf(char buf[PATH_MAX]) {
normalize_inplace(buf);
char *ret = GC_MALLOC_ATOMIC(strlen(buf) + 1);
memcpy(ret, buf, strlen(buf) + 1);
return ret;
}
public
PUREFUNC
Path_t Path$from_str(const char *str) {
if (!str || str[0] == '\0' || streq(str, "/")) return ROOT_PATH;
else if (streq(str, "~") || streq(str, "~/")) return HOME_PATH;
else if (streq(str, ".") || streq(str, "./")) return CURDIR_PATH;
return str;
}
public
Path_t Path$from_text(Text_t text) {
return Path$from_str(Text$as_c_string(text));
}
static OptionalPath_t Path$_concat2(OptionalPath_t a, OptionalPath_t b) {
if (a == NULL || b == NULL) return NULL;
if (path_type(b) != PATH_RELATIVE)
fail("Cannot concatenate an absolute or home-based path onto another path: (", b, ")");
if (b[0] == '.' && b[1] == '\0') return a;
static char buf[PATH_MAX];
snprintf(buf, sizeof(buf), "%s/%s", a, b);
return path_from_buf(buf);
}
public
Path_t Path$expand_home(Path_t path) {
if (path && path_type(path) == PATH_HOME) {
const char *home = getenv("HOME");
if (path[1] == '/') return Path$_concat2(home, path + 2);
else if (path[1] == '\0') return home;
}
return path;
}
public
OptionalPath_t Path$_concat(int n, Path_t items[n]) {
assert(n > 0);
OptionalPath_t result = items[0];
for (int i = 1; i < n; i++) {
result = Path$_concat2(result, items[i]);
}
return result;
}
public
Path_t Path$resolved(Path_t path, Path_t relative_to) {
if (!path) return path;
switch (path_type(path)) {
case PATH_HOME: return Path$expand_home(path);
case PATH_ABSOLUTE: return path;
case PATH_RELATIVE: return Path$_concat2(relative_to, path);
default: return path;
}
}
public
Path_t Path$relative_to(Path_t path, Path_t relative_to) {
if (path_type(path) == PATH_RELATIVE) return path;
path = Path$expand_home(path);
switch (path_type(relative_to)) {
case PATH_HOME: relative_to = Path$expand_home(relative_to); break;
case PATH_RELATIVE: relative_to = Path$resolved(relative_to, Path$current_dir()); break;
default: break;
}
int64_t shared = 0;
for (int64_t i = 0;; i++) {
if ((path[i] == '/' || path[i] == '\0') && (relative_to[i] == '/' || relative_to[i] == '\0')) {
shared = i;
}
if (path[i] != relative_to[i] || !path[i]) break;
}
Path_t path_remainder = path[shared] == '\0' ? "" : &path[shared + 1];
Path_t relative_remainder = relative_to[shared] == '\0' ? "" : &relative_to[shared + 1];
if (strlen(path_remainder) > 0 && strlen(relative_remainder) == 0) {
// "/foo/baz/qux" relative to "/foo/baz" => "qux"
return path_remainder;
}
static char buf[PATH_MAX];
char *dest = buf;
for (const char *p = relative_remainder; *p; p += strcspn(p, "/"), p += strspn(p, "/")) {
*(dest++) = '.';
*(dest++) = '.';
*(dest++) = '/';
}
memcpy(dest, path_remainder, strlen(path_remainder));
dest += strlen(path_remainder);
*dest = '\0';
return path_from_buf(buf);
}
public
bool Path$exists(Path_t path) {
path = Path$expand_home(path);
struct stat sb;
return (stat(path, &sb) == 0);
}
static INLINE int path_stat(Path_t path, bool follow_symlinks, struct stat *sb) {
path = Path$expand_home(path);
return follow_symlinks ? stat(path, sb) : lstat(path, sb);
}
public
bool Path$is_file(Path_t path, bool follow_symlinks) {
struct stat sb;
int status = path_stat(path, follow_symlinks, &sb);
if (status != 0) return false;
return (sb.st_mode & S_IFMT) == S_IFREG;
}
public
bool Path$is_directory(Path_t path, bool follow_symlinks) {
struct stat sb;
int status = path_stat(path, follow_symlinks, &sb);
if (status != 0) return false;
return (sb.st_mode & S_IFMT) == S_IFDIR;
}
public
bool Path$is_pipe(Path_t path, bool follow_symlinks) {
struct stat sb;
int status = path_stat(path, follow_symlinks, &sb);
if (status != 0) return false;
return (sb.st_mode & S_IFMT) == S_IFIFO;
}
public
bool Path$is_socket(Path_t path, bool follow_symlinks) {
struct stat sb;
int status = path_stat(path, follow_symlinks, &sb);
if (status != 0) return false;
return (sb.st_mode & S_IFMT) == S_IFSOCK;
}
public
bool Path$is_symlink(Path_t path) {
struct stat sb;
int status = path_stat(path, false, &sb);
if (status != 0) return false;
return (sb.st_mode & S_IFMT) == S_IFLNK;
}
public
OptionalPath_t Path$link(Path_t path) {
static char buf[PATH_MAX];
ssize_t status = readlink(path, buf, sizeof(buf));
if (status == -1) return NONE_PATH;
return Path$from_str(GC_strdup(buf));
}
public
bool Path$can_read(Path_t path) {
path = Path$expand_home(path);
#ifdef _GNU_SOURCE
return (euidaccess(path, R_OK) == 0);
#else
return (access(path, R_OK) == 0);
#endif
}
public
bool Path$can_write(Path_t path) {
path = Path$expand_home(path);
#ifdef _GNU_SOURCE
return (euidaccess(path, W_OK) == 0);
#else
return (access(path, W_OK) == 0);
#endif
}
public
bool Path$can_execute(Path_t path) {
path = Path$expand_home(path);
#ifdef _GNU_SOURCE
return (euidaccess(path, X_OK) == 0);
#else
return (access(path, X_OK) == 0);
#endif
}
public
OptionalInt64_t Path$modified(Path_t path, bool follow_symlinks) {
struct stat sb;
int status = path_stat(path, follow_symlinks, &sb);
if (status != 0) return NONE_INT64;
return (OptionalInt64_t){.value = (int64_t)sb.st_mtime};
}
public
OptionalInt64_t Path$accessed(Path_t path, bool follow_symlinks) {
struct stat sb;
int status = path_stat(path, follow_symlinks, &sb);
if (status != 0) return NONE_INT64;
return (OptionalInt64_t){.value = (int64_t)sb.st_atime};
}
public
OptionalInt64_t Path$changed(Path_t path, bool follow_symlinks) {
struct stat sb;
int status = path_stat(path, follow_symlinks, &sb);
if (status != 0) return NONE_INT64;
return (OptionalInt64_t){.value = (int64_t)sb.st_ctime};
}
static Result_t _write(Path_t path, List_t bytes, int mode, int permissions) {
path = Path$expand_home(path);
int fd = open(path, mode, permissions);
if (fd == -1) {
if (errno == EMFILE || errno == ENFILE) {
// If we hit file handle limits, run GC collection to try to clean up any lingering file handles that
// will be closed by GC finalizers.
GC_gcollect();
fd = open(path, mode, permissions);
}
if (fd == -1) return FailureResult("Could not write to file: ", path, " (", strerror(errno), ")");
}
if (bytes.stride != 1) List$compact(&bytes, 1);
ssize_t written = write(fd, bytes.data, (size_t)bytes.length);
if (written != (ssize_t)bytes.length)
return FailureResult("Could not write to file: ", path, " (", strerror(errno), ")");
close(fd);
return SuccessResult;
}
public
Result_t Path$write(Path_t path, Text_t text, int permissions) {
List_t bytes = Text$utf8(text);
return _write(path, bytes, O_WRONLY | O_CREAT | O_TRUNC, permissions);
}
public
Result_t Path$write_bytes(Path_t path, List_t bytes, int permissions) {
return _write(path, bytes, O_WRONLY | O_CREAT | O_TRUNC, permissions);
}
public
Result_t Path$append(Path_t path, Text_t text, int permissions) {
List_t bytes = Text$utf8(text);
return _write(path, bytes, O_WRONLY | O_APPEND | O_CREAT, permissions);
}
public
Result_t Path$append_bytes(Path_t path, List_t bytes, int permissions) {
return _write(path, bytes, O_WRONLY | O_APPEND | O_CREAT, permissions);
}
typedef struct {
const char *path;
int fd;
int mode;
int permissions;
} writer_data_t;
static Result_t _write_bytes_to_fd(List_t bytes, bool close_file, void *userdata) {
writer_data_t *data = userdata;
if (bytes.length > 0) {
if (data->fd == -1) {
data->fd = open(data->path, data->mode, data->permissions);
if (data->fd == -1) {
if (errno == EMFILE || errno == ENFILE) {
// If we hit file handle limits, run GC collection to try to clean up any lingering file handles
// that will be closed by GC finalizers.
GC_gcollect();
data->fd = open(data->path, data->mode, data->permissions);
}
if (data->fd == -1)
return FailureResult("Could not write to file: ", data->path, " (", strerror(errno), ")");
}
}
if (bytes.stride != 1) List$compact(&bytes, 1);
ssize_t written = write(data->fd, bytes.data, (size_t)bytes.length);
if (written != (ssize_t)bytes.length)
return FailureResult("Could not write to file: ", data->path, " (", strerror(errno), ")");
}
// After first successful write, all writes are appends
data->mode = (O_WRONLY | O_CREAT | O_APPEND);
if (close_file && data->fd != -1) {
if (close(data->fd) == -1)
return FailureResult("Failed to close file: ", data->path, " (", strerror(errno), ")");
data->fd = -1;
}
return SuccessResult;
}
static Result_t _write_text_to_fd(Text_t text, bool close_file, void *userdata) {
return _write_bytes_to_fd(Text$utf8(text), close_file, userdata);
}
static void _writer_cleanup(writer_data_t *data) {
if (data && data->fd != -1) {
close(data->fd);
data->fd = -1;
}
}
public
Closure_t Path$byte_writer(Path_t path, bool append, int permissions) {
path = Path$expand_home(path);
int mode = append ? (O_WRONLY | O_CREAT | O_APPEND) : (O_WRONLY | O_CREAT | O_TRUNC);
writer_data_t *userdata = new (writer_data_t, .fd = -1, .path = path, .mode = mode, .permissions = permissions);
GC_register_finalizer(userdata, (void *)_writer_cleanup, NULL, NULL, NULL);
return (Closure_t){.fn = _write_bytes_to_fd, .userdata = userdata};
}
public
Closure_t Path$writer(Path_t path, bool append, int permissions) {
path = Path$expand_home(path);
int mode = append ? (O_WRONLY | O_CREAT | O_APPEND) : (O_WRONLY | O_CREAT | O_TRUNC);
writer_data_t *userdata = new (writer_data_t, .fd = -1, .path = path, .mode = mode, .permissions = permissions);
GC_register_finalizer(userdata, (void *)_writer_cleanup, NULL, NULL, NULL);
return (Closure_t){.fn = _write_text_to_fd, .userdata = userdata};
}
public
OptionalList_t Path$read_bytes(Path_t path, OptionalInt_t count) {
path = Path$expand_home(path);
int fd = open(path, O_RDONLY);
if (fd == -1) {
if (errno == EMFILE || errno == ENFILE) {
// If we hit file handle limits, run GC collection to try to clean up any lingering file handles that
// will be closed by GC finalizers.
GC_gcollect();
fd = open(path, O_RDONLY);
}
}
if (fd == -1) return NONE_LIST;
struct stat sb;
if (fstat(fd, &sb) != 0) return NONE_LIST;
int64_t const target_count = count.small ? Int64$from_int(count, false) : INT64_MAX;
if (target_count < 0) fail("Cannot read a negative number of bytes!");
if ((sb.st_mode & S_IFMT) == S_IFREG) { // Use memory mapping if it's a real file:
const char *mem = mmap(NULL, (size_t)sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
char *content = GC_MALLOC_ATOMIC((size_t)sb.st_size + 1);
memcpy(content, mem, (size_t)sb.st_size);
content[sb.st_size] = '\0';
close(fd);
if (count.small && (int64_t)sb.st_size < target_count) return NONE_LIST;
int64_t len = count.small ? target_count : (int64_t)sb.st_size;
return (List_t){.data = content, .atomic = 1, .stride = 1, .length = (uint64_t)len};
} else {
size_t capacity = 256, len = 0;
char *content = GC_MALLOC_ATOMIC(capacity);
int64_t count_remaining = target_count;
for (;;) {
char chunk[256];
size_t to_read = count_remaining < (int64_t)sizeof(chunk) ? (size_t)count_remaining : sizeof(chunk);
ssize_t just_read = read(fd, chunk, to_read);
if (just_read < 0) {
close(fd);
return NONE_LIST;
} else if (just_read == 0) {
if (errno == EAGAIN || errno == EINTR) continue;
break;
}
count_remaining -= (int64_t)just_read;
if (len + (size_t)just_read >= capacity) {
content = GC_REALLOC(content, (capacity *= 2));
}
memcpy(&content[len], chunk, (size_t)just_read);
len += (size_t)just_read;
}
close(fd);
if (count.small != 0 && (int64_t)len < target_count) return NONE_LIST;
return (List_t){.data = content, .atomic = 1, .stride = 1, .length = (uint64_t)len};
}
}
public
OptionalText_t Path$read(Path_t path) {
List_t bytes = Path$read_bytes(path, NONE_INT);
if (bytes.data == NULL) return NONE_TEXT;
return Text$from_utf8(bytes);
}
public
OptionalText_t Path$owner(Path_t path, bool follow_symlinks) {
struct stat sb;
int status = path_stat(path, follow_symlinks, &sb);
if (status != 0) return NONE_TEXT;
struct passwd *pw = getpwuid(sb.st_uid);
return pw ? Text$from_str(pw->pw_name) : NONE_TEXT;
}
public
OptionalText_t Path$group(Path_t path, bool follow_symlinks) {
struct stat sb;
int status = path_stat(path, follow_symlinks, &sb);
if (status != 0) return NONE_TEXT;
struct group *gr = getgrgid(sb.st_uid);
return gr ? Text$from_str(gr->gr_name) : NONE_TEXT;
}
public
Result_t Path$set_owner(Path_t path, OptionalText_t owner, OptionalText_t group, bool follow_symlinks) {
uid_t owner_id = (uid_t)-1;
if (owner.tag == TEXT_NONE) {
struct passwd *pwd = getpwnam(Text$as_c_string(owner));
if (pwd == NULL) return FailureResult("Not a valid user: ", owner);
owner_id = pwd->pw_uid;
}
gid_t group_id = (gid_t)-1;
if (group.tag == TEXT_NONE) {
struct group *grp = getgrnam(Text$as_c_string(group));
if (grp == NULL) return FailureResult("Not a valid group: ", group);
group_id = grp->gr_gid;
}
int result = follow_symlinks ? chown(path, owner_id, group_id) : lchown(path, owner_id, group_id);
if (result < 0) return FailureResult("Could not set owner!");
return SuccessResult;
}
static int _remove_files(const char *path, const struct stat *sbuf, int type, struct FTW *ftwb) {
(void)sbuf, (void)ftwb;
switch (type) {
case FTW_F:
case FTW_SL:
case FTW_SLN:
if (remove(path) < 0) {
fail("Could not remove file: ", path, " (", strerror(errno), ")");
return -1;
}
return 0;
case FTW_DP:
if (rmdir(path) != 0) fail("Could not remove directory: ", path, " (", strerror(errno), ")");
return 0;
default: fail("Could not remove path: ", path, " (not a file or directory)"); return -1;
}
}
public
Result_t Path$remove(Path_t path, bool ignore_missing) {
path = Path$expand_home(path);
struct stat sb;
if (lstat(path, &sb) != 0) {
if (!ignore_missing) return FailureResult("Could not remove file: ", path, " (", strerror(errno), ")");
return SuccessResult;
}
if ((sb.st_mode & S_IFMT) == S_IFREG || (sb.st_mode & S_IFMT) == S_IFLNK) {
if (unlink(path) != 0 && !ignore_missing)
return FailureResult("Could not remove file: ", path, " (", strerror(errno), ")");
} else if ((sb.st_mode & S_IFMT) == S_IFDIR) {
const int num_open_fd = 10;
if (nftw(path, _remove_files, num_open_fd, FTW_DEPTH | FTW_MOUNT | FTW_PHYS) < 0)
return FailureResult("Could not remove directory: ", path, " (", strerror(errno), ")");
} else {
return FailureResult("Could not remove path: ", path, " (not a file or directory)");
}
return SuccessResult;
}
Result_t Path$move(Path_t src, Path_t dest, bool allow_overwriting) {
int status = rename(src, dest);
if (status != 0) {
if (errno == EEXIST && allow_overwriting) {
Result_t result = Path$remove(dest, true);
if (result.Failure.reason.tag != TEXT_NONE) return result;
return Path$move(src, dest, allow_overwriting);
}
return FailureResult("Could not move file ", src, " to ", dest, " (", strerror(errno), ")");
}
return SuccessResult;
}
Result_t Path$copy_to(Path_t src, Path_t dest, bool allow_overwriting) {
pid_t child = fork();
if (child == 0) {
const char *args[] = {"cp", allow_overwriting ? "-rf" : "-r", "-T", src, dest, NULL};
execvp("cp", (char **)args);
exit(0);
}
int status;
while (waitpid(child, &status, 0) < 0 && errno == EINTR) {
if (WIFEXITED(status) || WIFSIGNALED(status)) break;
else if (WIFSTOPPED(status)) kill(child, SIGCONT);
}
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
return FailureResult("Failed to copy ", src, " to ", dest);
}
return SuccessResult;
}
public
Result_t Path$create_directory(Path_t path, int permissions, bool recursive) {
retry:
path = Path$expand_home(path);
const char *c_path = Path$as_c_string(path);
int status = mkdir(c_path, (mode_t)permissions);
if (status != 0) {
if (recursive && errno == ENOENT) {
Path$create_directory(Path$parent(path), permissions, recursive);
goto retry;
} else if (errno != EEXIST) {
return FailureResult("Could not create directory: ", c_path, " (", strerror(errno), ")");
}
}
return SuccessResult;
}
static OptionalList_t _filtered_children(Path_t path, bool include_hidden, mode_t filter) {
path = Path$resolved(path, Path$current_dir());
List_t children = EMPTY_LIST;
size_t path_len = strlen(path);
DIR *d = opendir(path);
if (!d) return NONE_LIST;
if (path[path_len - 1] == '/') --path_len;
struct dirent *ent;
while ((ent = readdir(d)) != NULL) {
if (!include_hidden && ent->d_name[0] == '.') continue;
if (streq(ent->d_name, ".") || streq(ent->d_name, "..")) continue;
const char *child_str = String(string_slice(path, path_len), "/", ent->d_name);
struct stat sb;
if (stat(child_str, &sb) != 0) continue;
if (!((sb.st_mode & S_IFMT) & filter)) continue;
Path_t child = Path$from_str(child_str);
List$insert(&children, &child, I(0), sizeof(Path_t));
}
closedir(d);
return children;
}
public
OptionalList_t Path$children(Path_t path, bool include_hidden) {
return _filtered_children(path, include_hidden, (mode_t)-1);
}
public
OptionalList_t Path$files(Path_t path, bool include_hidden) {
return _filtered_children(path, include_hidden, S_IFREG);
}
public
OptionalList_t Path$subdirectories(Path_t path, bool include_hidden) {
return _filtered_children(path, include_hidden, S_IFDIR);
}
typedef struct {
Path_t path;
DIR *dir;
bool include_hidden : 1;
} child_info_t;
static OptionalPath_t _next_child(child_info_t *info) {
if (!info->dir) return NONE_PATH;
for (struct dirent *ent; (ent = readdir(info->dir)) != NULL;) {
if (!info->include_hidden && ent->d_name[0] == '.') continue;
if (streq(ent->d_name, ".") || streq(ent->d_name, "..")) continue;
Path_t child = Path$_concat2(info->path, ent->d_name);
return child;
}
closedir(info->dir);
info->dir = NULL;
return NONE_PATH;
}
public
Closure_t Path$each_child(Path_t path, bool include_hidden) {
path = Path$resolved(path, Path$current_dir());
DIR *d = opendir(path);
if (!d) return NONE_CLOSURE;
child_info_t *info = GC_malloc(sizeof(child_info_t));
info->path = path;
info->dir = d;
info->include_hidden = include_hidden;
return (Closure_t){.fn = (void *)_next_child, .userdata = info};
}
public
OptionalPath_t Path$unique_directory(Path_t path) {
path = Path$expand_home(path);
size_t len = strlen(path);
if (len >= PATH_MAX) fail("Path is too long: ", path);
static char buf[PATH_MAX] = {};
memcpy(buf, path, len);
buf[len] = '\0';
if (buf[len - 1] == '/') buf[--len] = '\0';
char *created = mkdtemp(buf);
if (!created) return NULL;
return Path$from_str(created);
}
public
OptionalPath_t Path$write_unique_bytes(Path_t path, List_t bytes) {
path = Path$expand_home(path);
size_t len = strlen(path);
if (len >= PATH_MAX) fail("Path is too long: ", path);
static char buf[PATH_MAX] = {};
memcpy(buf, path, len);
buf[len] = '\0';
// Count the number of trailing characters leading up to the last "X"
// (e.g. "foo_XXXXXX.tmp" would yield suffixlen = 4)
size_t suffixlen = 0;
while (suffixlen < len && buf[len - 1 - suffixlen] != 'X')
++suffixlen;
int fd = mkstemps(buf, suffixlen);
if (fd == -1) return NULL;
if (bytes.stride != 1) List$compact(&bytes, 1);
ssize_t written = write(fd, bytes.data, (size_t)bytes.length);
if (written != (ssize_t)bytes.length) fail("Could not write to file: ", buf, " (", strerror(errno), ")");
close(fd);
return Path$from_str(buf);
}
public
OptionalPath_t Path$write_unique(Path_t path, Text_t text) {
return Path$write_unique_bytes(path, Text$utf8(text));
}
public
OptionalPath_t Path$parent(Path_t path) {
if (!path || path[0] == '\0' || strspn(path, "/") == strlen(path)) {
// root dir has no parent
return NULL;
}
if (streq(path, ".")) return PARENT_PATH;
static char buf[PATH_MAX];
snprintf(buf, sizeof(buf), "%s/..", path);
return path_from_buf(buf);
}
static const char *base_name_start(Path_t path) {
if (!path || path[0] == '\0') return "";
const char *end = path + strlen(path);
// Strip trailing slash
while (end > path && end[0] == '/')
end -= 1;
// Get component up to end, excluding trailing slash
while (end > path && end[-1] != '/')
end -= 1;
return end;
}
public
PUREFUNC Text_t Path$base_name(Path_t path) {
const char *base = base_name_start(path);
return Text$from_strn(base, strcspn(base, "/"));
}
public
Text_t Path$extension(Path_t path, bool full) {
const char *base = base_name_start(path);
if (!base || base[0] == '\0') return EMPTY_TEXT;
if (base[0] == '.') base += 1;
const char *dot = full ? strchr(base + 1, '.') : strrchr(base + 1, '.');
const char *extension = dot ? dot + 1 : "";
return Text$from_strn(extension, strcspn(extension, "/"));
}
public
bool Path$has_extension(Path_t path, Text_t extension) {
const char *base = base_name_start(path);
if (!base || base[0] == '\0') return false;
if (base[0] == '.') base += 1;
const char *end = base;
while (*end && *end != '/')
end += 1;
int64_t base_len = (int64_t)(end - base);
if (base_len <= 0) return false;
if (extension.length == 0) {
const char *dot = strrchr(base, '.');
return dot == NULL || dot[1] == '\0' || dot == base;
}
const char *ext = Text$as_c_string(extension);
if (ext[0] == '.') {
if (1 + (int64_t)extension.length > base_len) return false;
return strncmp(base + base_len - extension.length, ext, extension.length) == 0;
} else {
if (1 + 1 + (int64_t)extension.length > base_len) return false;
return base[base_len - 1 - extension.length] == '.'
&& strncmp(base + base_len - extension.length, ext, extension.length) == 0;
}
}
public
List_t Path$components(Path_t path) {
char buf[PATH_MAX + 1] = {};
size_t len = MIN(strlen(path), PATH_MAX);
memcpy(buf, path, len);
buf[len] = '\0';
List_t components = EMPTY_LIST;
if (path[0] == '/') {
Text_t root = Text("/");
List$insert(&components, &root, I(0), sizeof(root));
}
for (char *comp = buf, *next = buf; (comp = strsep(&next, "/"));) {
if (comp[0] != '\0') {
Text_t comp_text = Text$from_str(comp);
List$insert(&components, &comp_text, I(0), sizeof(comp_text));
}
}
return components;
}
public
Path_t Path$child(Path_t path, Text_t name) {
static char buf[PATH_MAX];
snprintf(buf, sizeof(buf), "%s/%s", path, Text$as_c_string(name));
return path_from_buf(buf);
}
public
Path_t Path$sibling(Path_t path, Text_t name) {
static char buf[PATH_MAX];
snprintf(buf, sizeof(buf), "%s/../%s", path, Text$as_c_string(name));
return path_from_buf(buf);
}
public
OptionalPath_t Path$with_extension(Path_t path, Text_t extension, bool replace) {
if (!path || path[0] == '\0') return NULL;
static char buf[PATH_MAX];
const char *ext = Text$as_c_string(extension);
if (replace) {
char *base = (char *)base_name_start(path);
char *dot = base;
while (*dot && *dot != '.')
dot += 1;
if (ext[0] == '.' || ext[0] == '\0') snprintf(buf, sizeof(buf), "%.*s%s", (int)(dot - path), path, ext);
else snprintf(buf, sizeof(buf), "%.*s.%s", (int)(dot - path), path, ext);
} else {
if (ext[0] == '.' || ext[0] == '\0') snprintf(buf, sizeof(buf), "%s%s", path, ext);
else snprintf(buf, sizeof(buf), "%s.%s", path, ext);
}
return path_from_buf(buf);
}
static void _line_reader_cleanup(FILE **f) {
if (f && *f) {
fclose(*f);
*f = NULL;
}
}
static Text_t _next_line(FILE **f) {
if (!f || !*f) return NONE_TEXT;
char *line = NULL;
size_t size = 0;
next_line:;
ssize_t len = getline(&line, &size, *f);
if (len <= 0) {
if (line != NULL) free(line);
_line_reader_cleanup(f);
return NONE_TEXT;
}
while (len > 0 && (line[len - 1] == '\r' || line[len - 1] == '\n'))
--len;
if (u8_check((uint8_t *)line, (size_t)len) != NULL) {
// If there's invalid UTF8, skip this line and move to the next
goto next_line;
}
Text_t line_text = Text$from_strn(line, (size_t)len);
free(line);
return line_text;
}
public
OptionalClosure_t Path$by_line(Path_t path) {
path = Path$expand_home(path);
FILE *f = fopen(path, "r");
if (f == NULL) {
if (errno == EMFILE || errno == ENFILE) {
// If we hit file handle limits, run GC collection to try to clean up any lingering file handles that
// will be closed by GC finalizers.
GC_gcollect();
f = fopen(path, "r");
}
}
if (f == NULL) return NONE_CLOSURE;
FILE **wrapper = GC_MALLOC(sizeof(FILE *));
*wrapper = f;
GC_register_finalizer(wrapper, (void *)_line_reader_cleanup, NULL, NULL, NULL);
return (Closure_t){.fn = (void *)_next_line, .userdata = wrapper};
}
public
OptionalList_t Path$lines(Path_t path) {
FILE *f = fopen(path, "r");
if (f == NULL) {
if (errno == EMFILE || errno == ENFILE) {
// If we hit file handle limits, run GC collection to try to clean up any lingering file handles that
// will be closed by GC finalizers.
GC_gcollect();
f = fopen(path, "r");
}
}
if (f == NULL) return NONE_LIST;
List_t lines = EMPTY_LIST;
for (OptionalText_t line; (line = _next_line(&f)).tag != TEXT_NONE;) {
List$insert(&lines, &line, I(0), sizeof(line));
}
return lines;
}
public
List_t Path$glob(Path_t path) {
glob_t glob_result;
int status = glob(Path$as_c_string(path), GLOB_BRACE | GLOB_TILDE, NULL, &glob_result);
if (status != 0 && status != GLOB_NOMATCH) fail("Failed to perform globbing");
List_t glob_files = EMPTY_LIST;
for (size_t i = 0; i < glob_result.gl_pathc; i++) {
size_t len = strlen(glob_result.gl_pathv[i]);
if ((len >= 2 && glob_result.gl_pathv[i][len - 1] == '.' && glob_result.gl_pathv[i][len - 2] == '/')
|| (len >= 2 && glob_result.gl_pathv[i][len - 1] == '.' && glob_result.gl_pathv[i][len - 2] == '.'
&& glob_result.gl_pathv[i][len - 3] == '/'))
continue;
Path_t p = Path$from_str(glob_result.gl_pathv[i]);
List$insert(&glob_files, &p, I(0), sizeof(Path_t));
}
return glob_files;
}
public
bool Path$matches_glob(Path_t path, Text_t glob) {
return !fnmatch(Text$as_c_string(glob), path, FNM_PATHNAME | FNM_PERIOD);
}
public
Path_t Path$current_dir(void) {
static char cwd[PATH_MAX];
if (getcwd(cwd, sizeof(cwd)) == NULL) fail("Could not get current working directory");
return Path$from_str(cwd);
}
typedef struct {
List_t dir_stack;
OptionalPath_t current;
DIR *dir;
bool include_hidden : 1, follow_symlinks : 1;
} walk_info_t;
static OptionalPath_t _walk_next_path(walk_info_t *info) {
while (info->dir == NULL) {
if (info->dir_stack.length == 0) return NONE_PATH;
Path_t p = *(Path_t *)info->dir_stack.data;
List$remove_at(&info->dir_stack, I(1), I(1), sizeof(Path_t));
info->dir = opendir(p);
info->current = p;
return p;
}
for (struct dirent *ent; (ent = readdir(info->dir)) != NULL;) {
if (!info->include_hidden && ent->d_name[0] == '.') continue;
if (streq(ent->d_name, ".") || streq(ent->d_name, "..")) continue;
Path_t path = Path$_concat2(info->current, Path$from_str(ent->d_name));
if (Path$is_directory(path, info->follow_symlinks)) {
List$insert(&info->dir_stack, &path, I(0), sizeof(Path_t));
continue;
}
return path;
}
closedir(info->dir);
info->dir = NULL;
return _walk_next_path(info);
}
public
Closure_t Path$walk(Path_t dir, bool include_hidden, bool follow_symlinks) {
dir = Path$resolved(dir, Path$current_dir());
walk_info_t *info = GC_malloc(sizeof(walk_info_t));
info->dir_stack = List(dir);
info->current = dir;
info->dir = NULL;
info->include_hidden = include_hidden;
info->follow_symlinks = follow_symlinks;
return (Closure_t){.fn = (void *)_walk_next_path, .userdata = info};
}
public
CONSTFUNC
const char *Path$as_c_string(Path_t path) {
return path;
}
public
Text_t Path$as_text(const void *obj, bool color, const TypeInfo_t *type) {
(void)type;
if (!obj) return Text("Path");
Path_t *path = (Path_t *)obj;
Text_t text = Text$from_str(*path);
if (color) text = Text$concat(Text("\033[32;1m"), text, Text("\033[m"));
return text;
}
public
const TypeInfo_t Path$info = {
.size = sizeof(Path_t),
.align = __alignof__(Path_t),
.tag = OpaqueInfo,
.metamethods =
{
.as_text = Path$as_text,
.compare = CString$compare,
.equal = CString$equal,
.hash = CString$hash,
.is_none = CString$is_none,
.serialize = CString$serialize,
.deserialize = CString$deserialize,
},
};
|