12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466 | // 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 <limits.h>
#include <pwd.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "../unistr-fixed.h"
#include "../util.h"
#include "c_string.h"
#include "datatypes/typeinfo.h"
#include "integers.h"
#include "list.h"
#include "path.h"
#include "print.h"
#include "result.h"
#include "text.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] == '.' && buf[0] == '/' && dest == buf + 1) {
; // Nothing above the root: "/.." is "/", as Path$parent() agrees
} else 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 (keeping the
// root "/" when the popped component was the last one):
if (prev_slash >= buf && *prev_slash == '/'
&& strncmp(prev_slash, "/../", (size_t)(dest - prev_slash)) != 0) {
dest = (prev_slash == buf) ? buf + 1 : 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;
}
// normalize_inplace() wants a PATH_MAX buffer, so an over-long path fails here
// rather than being quietly truncated into a path naming some other file.
public
Path_t normalized_path(const char *str) {
size_t len = strlen(str);
if (len >= PATH_MAX) fail("Path is too long: ", str);
static char buf[PATH_MAX];
memcpy(buf, str, len + 1);
return path_from_buf(buf);
}
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) {
// Normalized, unlike Path$from_str(): this is a path being built out of
// arbitrary text, where a literal would have been normalized at compile
// time and every other constructor normalizes as it goes. Path$from_str()
// itself must not, being the way results from readdir(2) and glob(3) --
// already normal, and not to be second-guessed -- become paths.
return normalized_path(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;
return normalized_path(String(a, "/", b));
}
// $HOME is not guaranteed to be set. Returning NULL here would reach opendir()
// and friends by way of expand_home(), so fall back to the password database
// and fail loudly rather than crash if even that has no answer.
static const char *home_directory(void) {
const char *home = getenv("HOME");
if (home && home[0] != '\0') return home;
struct passwd *pw = getpwuid(getuid());
if (pw && pw->pw_dir && pw->pw_dir[0] != '\0') return pw->pw_dir;
fail("Could not determine the home directory: $HOME is unset and the user has no home");
}
public
Path_t Path$expand_home(Path_t path) {
if (path && path_type(path) == PATH_HOME) {
const char *home = home_directory();
if (path[1] == '/') return Path$_concat2(home, path + 2);
else if (path[1] == '\0') return home;
}
return path;
}
// The inverse of expand_home(), for methods that must hand libc a real path but
// should still hand the caller back a path in the form they wrote it.
static Path_t unexpand_home(Path_t path) {
if (!path) return path;
const char *home = home_directory();
size_t home_len = strlen(home);
while (home_len > 1 && home[home_len - 1] == '/')
--home_len;
if (strncmp(path, home, home_len) != 0) return path;
if (path[home_len] == '\0') return HOME_PATH;
if (path[home_len] != '/') return path;
return Path$from_str(String("~", path + home_len));
}
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:
// Path$relative_to() already does this to its own base:
if (path_type(relative_to) != PATH_ABSOLUTE) relative_to = Path$resolved(relative_to, Path$current_dir());
return Path$_concat2(relative_to, path);
default: return path;
}
}
public
Path_t Path$relative_to(Path_t path, Path_t relative_to) {
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;
}
path = Path$resolved(path, Path$current_dir());
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$expand_home(path), buf, sizeof(buf));
if (status == -1) return NONE_PATH;
return Path$from_str(GC_strdup(buf));
}
// AT_EACCESS makes these check against the process's *effective* user/group id
// (what would actually govern an attempted access), not the real one.
public
bool Path$can_read(Path_t path) {
path = Path$expand_home(path);
return (faccessat(AT_FDCWD, path, R_OK, AT_EACCESS) == 0);
}
public
bool Path$can_write(Path_t path) {
path = Path$expand_home(path);
return (faccessat(AT_FDCWD, path, W_OK, AT_EACCESS) == 0);
}
public
bool Path$can_execute(Path_t path) {
path = Path$expand_home(path);
return (faccessat(AT_FDCWD, path, X_OK, AT_EACCESS) == 0);
}
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)-1 tells chown(2) to leave that half alone, which is what a none
// owner or group means here.
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;
}
path = Path$expand_home(path);
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;
}
// nftw(3) hands its callback no user data, so a failure has to be left
// somewhere for Path$remove() to collect. _Thread_local rather than a plain
// static: the walk is not reentrant either way, but this keeps two of them from
// overwriting each other's reason. Returning non-zero stops the walk, so at
// most one failure is ever recorded -- the first one.
static _Thread_local Result_t _remove_failure = Result$Success;
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) {
_remove_failure = FailureResult("Could not remove file: ", path, " (", strerror(errno), ")");
return -1;
}
return 0;
case FTW_DP:
if (rmdir(path) != 0) {
_remove_failure = FailureResult("Could not remove directory: ", path, " (", strerror(errno), ")");
return -1;
}
return 0;
default:
// FTW_DNR (a directory that can't be read) and FTW_NS (a stat that
// failed) land here. Reporting them the same way as the cases above
// keeps a tree Path$remove() can't fully walk a Failure the caller
// sees, rather than an abort that takes the whole program down.
_remove_failure = FailureResult("Could not remove path: ", path, " (it could not be read)");
return -1;
}
}
public
Result_t Path$remove(Path_t path, bool ignore_missing) {
path = Path$expand_home(path);
// Attempt the removal and let the kernel report what is there, rather than
// asking with lstat() first: between the question and the answer the entry
// can be replaced, and unlink(2) does not follow symlinks, so a link that
// appears in that window is removed rather than followed.
if (unlink(path) == 0) return SuccessResult;
// ENOTDIR means a component of the path isn't a directory, so nothing can
// be at that path either:
if (errno == ENOENT || errno == ENOTDIR) {
if (ignore_missing) return SuccessResult;
return FailureResult("Could not remove file: ", path, " (", strerror(errno), ")");
}
// Linux reports EISDIR for a directory, the BSDs and macOS report EPERM:
if (errno == EISDIR || errno == EPERM) {
const int num_open_fd = 10;
_remove_failure = SuccessResult;
if (nftw(path, _remove_files, num_open_fd, FTW_DEPTH | FTW_MOUNT | FTW_PHYS) == 0) return SuccessResult;
// A file the walk could not remove, reported by the callback:
if (_remove_failure.$tag == Result$tag$Failure) return _remove_failure;
if (errno == ENOENT && ignore_missing) return SuccessResult;
return FailureResult("Could not remove directory: ", path, " (", strerror(errno), ")");
}
// `ignore_missing` is about a path that isn't there, which is settled
// above. Everything left (EACCES, EROFS, EBUSY, ...) is a file that is
// still there and has to be reported, or the caller is told the removal
// succeeded while the file remains.
return FailureResult("Could not remove file: ", path, " (", strerror(errno), ")");
}
#ifndef RENAME_NOREPLACE
#define RENAME_NOREPLACE (1 << 0)
#endif
// rename(2) replaces an existing destination without complaint. Checking with
// lstat() first only narrows the window -- another process can create the
// destination in between -- so the refusal has to happen inside one operation
// the kernel performs indivisibly.
static int rename_without_replacing(const char *src, const char *dest) {
#if defined(__linux__) && defined(SYS_renameat2)
int status = (int)syscall(SYS_renameat2, AT_FDCWD, src, AT_FDCWD, dest, RENAME_NOREPLACE);
// ENOSYS is a kernel too old for it; EINVAL is a filesystem that does not
// implement the flag. Anything else is the real answer, EEXIST included.
if (status == 0 || (errno != ENOSYS && errno != EINVAL)) return status;
#elif defined(__APPLE__)
int status = renameatx_np(AT_FDCWD, src, AT_FDCWD, dest, RENAME_EXCL);
if (status == 0 || errno != ENOTSUP) return status;
#endif
// link(2) fails with EEXIST rather than replacing anything, so it gives the
// same guarantee for everything it can handle:
if (link(src, dest) == 0) {
if (unlink(src) != 0) {
// The move didn't happen, so don't leave the extra link behind as
// a second name for the same file:
int unlink_errno = errno;
unlink(dest);
errno = unlink_errno;
return -1;
}
return 0;
}
// It cannot handle directories (EPERM) or a move across filesystems
// (EXDEV). Those fall back to looking first, which races; every other
// errno, EEXIST included, is the answer.
if (errno != EPERM && errno != EXDEV) return -1;
struct stat sb;
if (lstat(dest, &sb) == 0) {
errno = EEXIST;
return -1;
}
return rename(src, dest);
}
public
Result_t Path$move(Path_t src, Path_t dest, bool allow_overwriting) {
src = Path$expand_home(src);
dest = Path$expand_home(dest);
int status = allow_overwriting ? rename(src, dest) : rename_without_replacing(src, dest);
if (status != 0) {
// Replacing a non-empty directory needs it gone first; rename(2) will
// not do that for us:
if (allow_overwriting && (errno == EEXIST || errno == ENOTEMPTY || errno == EISDIR)) {
Result_t result = Path$remove(dest, true);
if (result.Failure.reason.tag != TEXT_NONE) return result;
if (rename(src, dest) == 0) return SuccessResult;
} else if (errno == EEXIST || errno == ENOTEMPTY) {
return FailureResult("Could not move file ", src, " to ", dest, " (the destination already exists)");
}
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) {
// There is no shell in between to expand a "~" for us:
src = Path$expand_home(src);
dest = Path$expand_home(dest);
// Copying a tree is many operations, so unlike Path$move() it cannot be one
// indivisible act. What protects the destination is "cp -n" below, which
// declines to clobber; this look is only so the caller is told that it
// declined, since cp exits 0 either way. If the destination appears after
// this and before the copy, "cp -n" still refuses and the caller is told
// Success -- an inaccurate report, never a lost file.
struct stat sb;
if (!allow_overwriting && lstat(dest, &sb) == 0)
return FailureResult("Could not copy ", src, " to ", dest, " (the destination already exists)");
pid_t child = fork();
if (child == 0) {
// "-r" alone still overwrites: "-f" only forces past a destination
// that cannot be opened. "-n" is the flag that declines to clobber.
const char *args[] = {"cp", allow_overwriting ? "-rf" : "-rn", "-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) {
// mkdir(2) is the check and the creation in one, which is why
// nothing looks first. But EEXIST only says something is there,
// so confirm it is the directory the caller asked for:
struct stat sb;
if (stat(c_path, &sb) != 0 || (sb.st_mode & S_IFMT) != S_IFDIR)
return FailureResult("Could not create directory: ", c_path,
" (something that is not a directory is already there)");
} else {
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) {
List_t children = EMPTY_LIST;
size_t path_len = strlen(path);
DIR *d = opendir(Path$expand_home(path));
if (!d) return NONE_LIST;
if (path_len > 0 && 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;
Path_t child = Path$from_str(String(string_slice(path, path_len), "/", ent->d_name));
struct stat sb;
if (stat(Path$expand_home(child), &sb) != 0) continue;
if (!((sb.st_mode & S_IFMT) & filter)) continue;
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
OptionalClosure_t Path$each_child(Path_t path, bool include_hidden) {
DIR *d = opendir(Path$expand_home(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) {
bool home_based = (path_type(path) == PATH_HOME);
path = Path$expand_home(path);
size_t len = strlen(path);
// A trailing slash would leave mkdtemp(3) with a template that names a
// directory inside one that doesn't exist yet, so trim it first:
while (len > 1 && path[len - 1] == '/')
--len;
// mkdtemp(3) replaces a trailing "XXXXXX" with the random characters, so
// a path that doesn't already end in one gets "-XXXXXX" appended:
const char *placeholder = "XXXXXX";
const size_t plen = strlen(placeholder);
bool has_placeholder = (len >= plen && strncmp(&path[len - plen], placeholder, plen) == 0);
size_t result_len = has_placeholder ? len : len + 1 + plen;
if (result_len >= PATH_MAX) fail("Path is too long: ", path);
char *result = GC_MALLOC_ATOMIC(result_len + 1);
memcpy(result, path, len);
if (!has_placeholder) {
result[len] = '-';
memcpy(&result[len + 1], placeholder, plen);
}
result[result_len] = '\0';
if (mkdtemp(result) == NULL) return NULL;
return home_based ? unexpand_home((Path_t)result) : (Path_t)result;
}
public
OptionalPath_t Path$write_unique_bytes(Path_t path, List_t bytes) {
bool home_based = (path_type(path) == PATH_HOME);
path = Path$expand_home(path);
const char *placeholder = "XXXXXX";
const char *placeholder_pos = strstr(path, placeholder);
size_t suffixlen = placeholder_pos ? strlen(placeholder_pos + strlen(placeholder)) : 0;
// If there is no "XXXXXX" then append "-XXXXXX"
if (!placeholder_pos) {
path = String(path, "-", placeholder);
}
size_t len = strlen(path);
if (len >= PATH_MAX) fail("Path is too long: ", path);
char *result = GC_MALLOC_ATOMIC(len + 1);
memcpy(result, path, len);
result[len] = '\0';
int fd = mkstemps(result, 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: ", result, " (", strerror(errno), ")");
close(fd);
return home_based ? unexpand_home((Path_t)result) : (Path_t)result;
}
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;
return normalized_path(String(path, "/.."));
}
static const char *base_name_start(Path_t path) {
if (!path || path[0] == '\0') return "";
const char *end = path + strlen(path);
// Strip trailing slashes. `end` starts on the terminator, so this looks at
// the byte behind it; testing end[0] only ever found the '\0'.
while (end > path && end[-1] == '/')
end -= 1;
// Get component up to end, excluding trailing slash
while (end > path && end[-1] != '/')
end -= 1;
return end;
}
public
PUREFUNC OptionalText_t Path$base_name(Path_t path) {
// A POSIX filename is any byte sequence, so this is none when the name is
// not valid UTF-8. Text$from_strn() already reports that; it used to be
// discarded by a non-optional return type.
const char *base = base_name_start(path);
return Text$from_strn(base, strcspn(base, "/"));
}
public
OptionalText_t Path$extension(Path_t path, bool full) {
const char *base = base_name_start(path);
if (!base || base[0] == '\0') return NONE_TEXT;
if (base[0] == '.') base += 1;
// Nothing after the leading ".", as in (.) or (..): no extension. Without
// this, the searches below would start one byte past the terminator.
if (base[0] == '\0') return NONE_TEXT;
const char *dot = full ? strchr(base + 1, '.') : strrchr(base + 1, '.');
if (!dot) return NONE_TEXT; // No "." in the name at all
const char *extension = dot + 1;
size_t len = strcspn(extension, "/");
if (len == 0) return NONE_TEXT; // A trailing "." is not an extension
return Text$from_strn(extension, len);
}
public
bool Path$has_extension(Path_t path, Text_t extension) {
const char *base = base_name_start(path);
// A path with no base name, such as the root, has no extension, so it
// answers yes only to the "does it lack one?" question:
if (!base || base[0] == '\0') return extension.length == 0;
if (base[0] == '.') base += 1;
const char *end = base;
while (*end && *end != '/')
end += 1;
int64_t base_len = (int64_t)(end - base);
// Nothing after the leading ".", as in (.) itself: no extension.
if (base_len <= 0) return extension.length == 0;
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
OptionalList_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') {
OptionalText_t comp_text = Text$from_str(comp);
// One undecodable component makes the whole split meaningless,
// rather than leaving a silent "" in the middle of the list:
if (comp_text.tag == TEXT_NONE) return NONE_LIST;
List$insert(&components, &comp_text, I(0), sizeof(comp_text));
}
}
return components;
}
public
Path_t Path$child(Path_t path, Text_t name) {
return normalized_path(String(path, "/", Text$as_c_string(name)));
}
public
OptionalPath_t Path$sibling(Path_t path, Text_t name) {
// Written as the ".parent().child(name)" it is documented to be, rather
// than by appending "/../name": appending gave the root a sibling, while
// Path$parent() says the root has no parent to put one beside.
OptionalPath_t parent = Path$parent(path);
if (parent == NULL) return NONE_PATH;
return Path$child(parent, name);
}
public
Path_t Path$with_extension(Path_t path, Text_t extension, bool replace) {
// Path$from_str() maps "" to "/", so a well-typed Path is never empty and
// this never fires; the declared type stays non-optional rather than making
// every caller unwrap a failure that cannot happen.
if (!path || path[0] == '\0') fail("Cannot set the extension of an empty path");
const char *ext = Text$as_c_string(extension);
const char *dot_or_empty = (ext[0] == '.' || ext[0] == '\0') ? "" : ".";
if (replace) {
const char *base = base_name_start(path);
const char *dot = base;
while (*dot && *dot != '.')
dot += 1;
return normalized_path(String(string_slice(path, (size_t)(dot - path)), dot_or_empty, ext));
} else {
return normalized_path(String(path, dot_or_empty, ext));
}
}
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) {
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_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;
}
// Split a path or a glob pattern into its "/"-separated pieces. Nothing here
// decodes: a filename that is not valid UTF-8 still has to be matchable, for
// the same reason Path$has_extension works on bytes.
//
// A leading "/" becomes a piece of its own. That is what anchors absolute
// patterns: no other piece can ever equal "/", so a pattern beginning with one
// can only line up at the front of the path.
static size_t split_components(const char *str, string_slice_t **out) {
size_t count = (str[0] == '/') ? 1 : 0;
for (const char *p = str; *p;) {
p += strspn(p, "/"); // Skip separators, including a run of them
if (!*p) break;
count += 1;
p += strcspn(p, "/");
}
string_slice_t *comps = GC_MALLOC(count * sizeof(string_slice_t));
size_t i = 0;
if (str[0] == '/') comps[i++] = string_slice("/", 1);
for (const char *p = str; *p && i < count;) {
p += strspn(p, "/");
if (!*p) break;
size_t len = strcspn(p, "/");
comps[i++] = string_slice(p, len);
p += len;
}
*out = comps;
return count;
}
static bool component_matches(string_slice_t path, string_slice_t glob) {
// fnmatch() needs NUL-terminated strings, and a slice is not one:
return fnmatch(String(glob), String(path), FNM_PATHNAME | FNM_PERIOD) == 0;
}
static bool is_double_star(string_slice_t comp) {
return comp.length == 2 && comp.str[0] == '*' && comp.str[1] == '*';
}
// The pattern has to consume every component it is given. "**" stands for zero
// or more components, so it tries each split point.
static bool match_components(string_slice_t *path, size_t n_path, string_slice_t *glob, size_t n_glob) {
if (n_glob == 0) return n_path == 0;
if (is_double_star(glob[0])) {
for (size_t skip = 0; skip <= n_path; skip++)
if (match_components(path + skip, n_path - skip, glob + 1, n_glob - 1)) return true;
return false;
}
if (n_path == 0) return false;
if (!component_matches(path[0], glob[0])) return false;
return match_components(path + 1, n_path - 1, glob + 1, n_glob - 1);
}
// Path$matches_glob() lets a pattern match any trailing run of components. A
// glob is anchored instead: the pattern is relative to the directory being
// globbed, so it has to account for every component below it.
//
// The walk compares one pattern against every path it reaches, so it splits
// that pattern a single time, up front, into the NUL-terminated pieces that
// fnmatch() takes.
static size_t split_cstring_components(const char *str, const char ***out) {
string_slice_t *slices;
size_t n = split_components(str, &slices);
const char **comps = GC_MALLOC(n * sizeof(const char *));
for (size_t i = 0; i < n; i++)
comps[i] = String(slices[i]);
*out = comps;
return n;
}
// "**" is special only as a component of its own. Anything else containing two
// stars -- "**.c", "**b" -- is an ordinary one-component pattern, where the
// adjacent stars collapse and fnmatch() reads it the same as "*.c" or "*b".
static bool walk_matches(const char **path, size_t n_path, const char **glob, size_t n_glob) {
if (n_glob == 0) return n_path == 0;
if (streq(glob[0], "**")) {
for (size_t skip = 0; skip <= n_path; skip++)
if (walk_matches(path + skip, n_path - skip, glob + 1, n_glob - 1)) return true;
return false;
}
if (n_path == 0) return false;
if (fnmatch(glob[0], path[0], FNM_PATHNAME | FNM_PERIOD) != 0) return false;
return walk_matches(path + 1, n_path - 1, glob + 1, n_glob - 1);
}
// Whether anything *below* these components could still match, which is what
// decides if a directory is worth opening. This is what keeps the walk to the
// part of the tree a pattern can reach: a pattern anchored to one subdirectory
// only ever descends towards it.
static bool walk_can_descend(const char **path, size_t n_path, const char **glob, size_t n_glob) {
if (n_glob == 0) return false; // The pattern is spent; nothing deeper fits
if (n_path == 0) return true; // Pattern left over, so deeper paths may fit
if (streq(glob[0], "**")) return true; // "**" covers any depth from here
if (fnmatch(glob[0], path[0], FNM_PATHNAME | FNM_PERIOD) != 0) return false;
return walk_can_descend(path + 1, n_path - 1, glob + 1, n_glob - 1);
}
// Backslash-escape every glob(3) metacharacter in `str`, so that the bytes are
// matched literally. glob(3) honours these escapes unless GLOB_NOESCAPE is set.
static const char *glob_escaped(const char *str, size_t len) {
char *escaped = GC_MALLOC_ATOMIC(2 * len + 1); // Worst case: every byte escaped
char *dest = escaped;
for (const char *src = str, *end = str + len; src < end; src++) {
if (*src == '*' || *src == '?' || *src == '[' || *src == ']' || *src == '\\') *(dest++) = '\\';
*(dest++) = *src;
}
*dest = '\0';
return escaped;
}
// The path components walked so far, relative to the directory being globbed.
// One buffer is reused for the whole walk: each level pushes a name before
// recursing and pops it after.
typedef struct {
const char **comps;
size_t length, capacity;
} walk_stack_t;
// glob(3) has no way to say "any number of directories", so a pattern with a
// "**" in it is answered by walking the tree and testing each path instead. A
// directory is opened when the pattern could still match something inside it,
// and every entry it holds is tested, hidden ones included: the matcher is what
// decides, and there "**" spans any component while "*" still will not match a
// leading ".".
static void glob_walk(Path_t dir, walk_stack_t *stack, const char **glob, size_t n_glob, List_t *out) {
DIR *d = opendir(Path$expand_home(dir));
if (!d) return;
size_t dir_len = strlen(dir);
if (dir_len > 0 && dir[dir_len - 1] == '/') --dir_len;
for (struct dirent *ent; (ent = readdir(d)) != NULL;) {
if (streq(ent->d_name, ".") || streq(ent->d_name, "..")) continue;
if (stack->length >= stack->capacity) {
stack->capacity = stack->capacity ? 2 * stack->capacity : 16;
stack->comps = GC_REALLOC(stack->comps, stack->capacity * sizeof(const char *));
}
stack->comps[stack->length++] = ent->d_name;
bool matched = walk_matches(stack->comps, stack->length, glob, n_glob);
bool descend = walk_can_descend(stack->comps, stack->length, glob, n_glob);
// Building the path costs an allocation, so only pay for it if the
// entry is either an answer or a directory worth looking inside:
if (matched || descend) {
Path_t child = Path$from_str(String(string_slice(dir, dir_len), "/", ent->d_name));
if (matched) List$insert(out, &child, I(0), sizeof(Path_t));
if (descend && Path$is_directory(child, false)) glob_walk(child, stack, glob, n_glob, out);
}
stack->length -= 1;
}
closedir(d);
}
static bool is_readable_directory(Path_t path) {
DIR *d = opendir(Path$expand_home(path));
if (d == NULL) return false;
closedir(d);
return true;
}
public
OptionalList_t Path$glob(Path_t path, Text_t pattern) {
const char *pat = Text$as_c_string(pattern);
if (pat[0] == '\0') return EMPTY_LIST; // An empty pattern matches nothing
// The directory has to be readable, which is a failure rather than an empty
// result, the same as Path$children. Finding matches proves it was, so the
// question is only asked when there were none -- checking first would be a
// second look at a directory that can change between the two.
// Whether to walk is decided by the same split components the walk itself
// would match with, so the two can never disagree about what a "**" is.
const char **glob_comps;
size_t n_glob = split_cstring_components(pat, &glob_comps);
bool has_double_star = false;
for (size_t i = 0; i < n_glob; i++)
if (streq(glob_comps[i], "**")) {
has_double_star = true;
break;
}
List_t glob_files = EMPTY_LIST;
if (has_double_star) {
walk_stack_t stack = {.comps = NULL, .length = 0, .capacity = 0};
glob_walk(path, &stack, glob_comps, n_glob, &glob_files);
if (glob_files.length == 0 && !is_readable_directory(path)) return NONE_LIST;
Closure_t comparison = {.fn = (void *)CString$compare, .userdata = (void *)&Path$info};
List$sort(&glob_files, comparison, sizeof(Path_t));
return glob_files;
}
// No "**", so let glob(3) prune as it descends instead of walking it all.
bool home_based = (path_type(path) == PATH_HOME);
Path_t expanded = Path$expand_home(path);
size_t dir_len = strlen(expanded);
if (dir_len > 0 && expanded[dir_len - 1] == '/') --dir_len;
// Only `pattern` is a pattern: the directory is a literal path, so a
// directory named "foo[1]" has to be escaped or glob(3) would read the
// "[1]" as a character class and match nothing inside it.
const char *joined = String(glob_escaped(expanded, dir_len), "/", pat);
glob_t glob_result;
int status = glob(joined, 0, NULL, &glob_result);
if (status != 0 && status != GLOB_NOMATCH) fail("Failed to perform globbing: ", joined);
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;
// Copy: Path$from_str() does not, and globfree() frees gl_pathv.
Path_t p = Path$from_str(GC_strdup(glob_result.gl_pathv[i]));
if (home_based) p = unexpand_home(p);
List$insert(&glob_files, &p, I(0), sizeof(Path_t));
}
globfree(&glob_result);
if (glob_files.length == 0 && !is_readable_directory(path)) return NONE_LIST;
// glob(3) sorts with strcoll() and the walk above does not sort at all, so
// both are put in the same order here rather than the answer depending on
// which branch produced it.
Closure_t comparison = {.fn = (void *)CString$compare, .userdata = (void *)&Path$info};
List$sort(&glob_files, comparison, sizeof(Path_t));
return glob_files;
}
public
bool Path$matches_glob(Path_t path, Text_t glob) {
string_slice_t *path_comps, *glob_comps;
size_t n_path = split_components(path, &path_comps);
size_t n_glob = split_components(Text$as_c_string(glob), &glob_comps);
if (n_glob == 0) return false; // An empty pattern matches nothing
// A pattern matches any trailing run of components, so "*.txt" asks about
// the file's name wherever it sits, while "/tmp/*.txt" pins the whole path.
for (size_t start = 0; start <= n_path; start++)
if (match_components(path_comps + start, n_path - start, glob_comps, n_glob)) return true;
return false;
}
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(GC_strdup(cwd)); // Copy out of the static buffer
}
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(Path$expand_home(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
OptionalClosure_t Path$walk(Path_t dir, bool include_hidden, bool follow_symlinks) {
// There is nothing to walk if the path is not there. lstat(), not stat():
// a dangling symlink is still an entry, and walk() yields the path itself
// before descending into it.
struct stat sb;
if (lstat(Path$expand_home(dir), &sb) != 0) return NONE_CLOSURE;
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
List_t Path$bytes(Path_t path) {
return CString$bytes(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[92;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,
},
};
|