1 // A lang for filesystem paths
16 #include <sys/param.h>
18 #include <sys/types.h>
21 #include "../unistr-fixed.h"
22 #include "c_strings.h"
26 #include "optionals.h"
34 static const Path_t HOME_PATH = (Path_t){"~"}, ROOT_PATH = (Path_t){"/"}, CURDIR_PATH = (Path_t){"."},
35 PARENT_PATH = (Path_t){".."};
37 typedef enum { PATH_ABSOLUTE, PATH_RELATIVE, PATH_HOME } pathtype_t;
39 static pathtype_t path_type(Path_t path) {
40 if (!path) return PATH_ABSOLUTE;
41 if (path[0] == '/') return PATH_ABSOLUTE;
42 if (path[0] == '~' && (path[1] == '\0' || path[1] == '/')) return PATH_HOME;
46 static void normalize_inplace(char path[PATH_MAX]) {
47 if (path[0] == '.' && path[1 + strspn(path + 1, "/")] == '\0') {
52 static char buf[PATH_MAX];
53 char *src = path, *dest = buf;
61 for (size_t component_len; *src != '\0' && dest < &buf[PATH_MAX - 1]; src += component_len + 1) {
62 component_len = strcspn(src, "/");
63 if (component_len == 0) {
64 ; // Skip empty "//"s:
65 } else if (component_len == 1 && src[0] == '.' && dest > buf) {
66 ; // Skip "." components
68 // Add "/" if there's a previous non-slash
69 if (dest > buf && dest[-1] != '/') {
74 if (component_len == 2 && src[0] == '.' && src[1] == '.') {
75 // Find previous component:
76 char *prev_slash = dest - 2;
77 while (prev_slash >= buf && *prev_slash != '/')
80 // If previous component is not "..", then pop it
81 if (prev_slash > buf && *prev_slash == '/'
82 && strncmp(prev_slash, "/../", (size_t)(dest - prev_slash)) != 0) {
85 // Otherwise we need to keep the ".."
90 // Otherwise copy over the component
91 memcpy(dest, src, component_len);
92 dest += component_len;
96 if (src[component_len] == '\0') break;
105 memcpy(path, buf, strlen(buf) + 1);
109 char *path_from_buf(char buf[PATH_MAX]) {
110 normalize_inplace(buf);
111 char *ret = GC_MALLOC_ATOMIC(strlen(buf) + 1);
112 memcpy(ret, buf, strlen(buf) + 1);
118 Path_t Path$from_str(const char *str) {
119 if (!str || str[0] == '\0' || streq(str, "/")) return ROOT_PATH;
120 else if (streq(str, "~") || streq(str, "~/")) return HOME_PATH;
121 else if (streq(str, ".") || streq(str, "./")) return CURDIR_PATH;
126 Path_t Path$from_text(Text_t text) {
127 return Path$from_str(Text$as_c_string(text));
130 static OptionalPath_t Path$_concat2(OptionalPath_t a, OptionalPath_t b) {
131 if (a == NULL || b == NULL) return NULL;
132 if (path_type(b) != PATH_RELATIVE)
133 fail("Cannot concatenate an absolute or home-based path onto another path: (", b, ")");
134 if (b[0] == '.' && b[1] == '\0') return a;
135 static char buf[PATH_MAX];
136 snprintf(buf, sizeof(buf), "%s/%s", a, b);
137 return path_from_buf(buf);
141 Path_t Path$expand_home(Path_t path) {
142 if (path && path_type(path) == PATH_HOME) {
143 const char *home = getenv("HOME");
144 if (path[1] == '/') return Path$_concat2(home, path + 2);
145 else if (path[1] == '\0') return home;
151 OptionalPath_t Path$_concat(int n, Path_t items[n]) {
153 OptionalPath_t result = items[0];
154 for (int i = 1; i < n; i++) {
155 result = Path$_concat2(result, items[i]);
161 Path_t Path$resolved(Path_t path, Path_t relative_to) {
162 if (!path) return path;
163 switch (path_type(path)) {
164 case PATH_HOME: return Path$expand_home(path);
165 case PATH_ABSOLUTE: return path;
166 case PATH_RELATIVE: return Path$_concat2(relative_to, path);
167 default: return path;
172 Path_t Path$relative_to(Path_t path, Path_t relative_to) {
173 if (path_type(path) == PATH_RELATIVE) return path;
175 path = Path$expand_home(path);
177 switch (path_type(relative_to)) {
178 case PATH_HOME: relative_to = Path$expand_home(relative_to); break;
179 case PATH_RELATIVE: relative_to = Path$resolved(relative_to, Path$current_dir()); break;
184 for (int64_t i = 0;; i++) {
185 if ((path[i] == '/' || path[i] == '\0') && (relative_to[i] == '/' || relative_to[i] == '\0')) {
188 if (path[i] != relative_to[i] || !path[i]) break;
191 Path_t path_remainder = path[shared] == '\0' ? "" : &path[shared + 1];
192 Path_t relative_remainder = relative_to[shared] == '\0' ? "" : &relative_to[shared + 1];
193 if (strlen(path_remainder) > 0 && strlen(relative_remainder) == 0) {
194 // "/foo/baz/qux" relative to "/foo/baz" => "qux"
195 return path_remainder;
198 static char buf[PATH_MAX];
200 for (const char *p = relative_remainder; *p; p += strcspn(p, "/"), p += strspn(p, "/")) {
205 memcpy(dest, path_remainder, strlen(path_remainder));
206 dest += strlen(path_remainder);
208 return path_from_buf(buf);
212 bool Path$exists(Path_t path) {
213 path = Path$expand_home(path);
215 return (stat(path, &sb) == 0);
218 static INLINE int path_stat(Path_t path, bool follow_symlinks, struct stat *sb) {
219 path = Path$expand_home(path);
220 return follow_symlinks ? stat(path, sb) : lstat(path, sb);
224 bool Path$is_file(Path_t path, bool follow_symlinks) {
226 int status = path_stat(path, follow_symlinks, &sb);
227 if (status != 0) return false;
228 return (sb.st_mode & S_IFMT) == S_IFREG;
232 bool Path$is_directory(Path_t path, bool follow_symlinks) {
234 int status = path_stat(path, follow_symlinks, &sb);
235 if (status != 0) return false;
236 return (sb.st_mode & S_IFMT) == S_IFDIR;
240 bool Path$is_pipe(Path_t path, bool follow_symlinks) {
242 int status = path_stat(path, follow_symlinks, &sb);
243 if (status != 0) return false;
244 return (sb.st_mode & S_IFMT) == S_IFIFO;
248 bool Path$is_socket(Path_t path, bool follow_symlinks) {
250 int status = path_stat(path, follow_symlinks, &sb);
251 if (status != 0) return false;
252 return (sb.st_mode & S_IFMT) == S_IFSOCK;
256 bool Path$is_symlink(Path_t path) {
258 int status = path_stat(path, false, &sb);
259 if (status != 0) return false;
260 return (sb.st_mode & S_IFMT) == S_IFLNK;
264 OptionalPath_t Path$link(Path_t path) {
265 static char buf[PATH_MAX];
266 ssize_t status = readlink(path, buf, sizeof(buf));
267 if (status == -1) return NONE_PATH;
268 return Path$from_str(GC_strdup(buf));
272 bool Path$can_read(Path_t path) {
273 path = Path$expand_home(path);
275 return (euidaccess(path, R_OK) == 0);
277 return (access(path, R_OK) == 0);
282 bool Path$can_write(Path_t path) {
283 path = Path$expand_home(path);
285 return (euidaccess(path, W_OK) == 0);
287 return (access(path, W_OK) == 0);
292 bool Path$can_execute(Path_t path) {
293 path = Path$expand_home(path);
295 return (euidaccess(path, X_OK) == 0);
297 return (access(path, X_OK) == 0);
302 OptionalInt64_t Path$modified(Path_t path, bool follow_symlinks) {
304 int status = path_stat(path, follow_symlinks, &sb);
305 if (status != 0) return NONE_INT64;
306 return (OptionalInt64_t){.value = (int64_t)sb.st_mtime};
310 OptionalInt64_t Path$accessed(Path_t path, bool follow_symlinks) {
312 int status = path_stat(path, follow_symlinks, &sb);
313 if (status != 0) return NONE_INT64;
314 return (OptionalInt64_t){.value = (int64_t)sb.st_atime};
318 OptionalInt64_t Path$changed(Path_t path, bool follow_symlinks) {
320 int status = path_stat(path, follow_symlinks, &sb);
321 if (status != 0) return NONE_INT64;
322 return (OptionalInt64_t){.value = (int64_t)sb.st_ctime};
325 static Result_t _write(Path_t path, List_t bytes, int mode, int permissions) {
326 path = Path$expand_home(path);
327 int fd = open(path, mode, permissions);
329 if (errno == EMFILE || errno == ENFILE) {
330 // If we hit file handle limits, run GC collection to try to clean up any lingering file handles that
331 // will be closed by GC finalizers.
333 fd = open(path, mode, permissions);
335 if (fd == -1) return FailureResult("Could not write to file: ", path, " (", strerror(errno), ")");
338 if (bytes.stride != 1) List$compact(&bytes, 1);
339 ssize_t written = write(fd, bytes.data, (size_t)bytes.length);
340 if (written != (ssize_t)bytes.length)
341 return FailureResult("Could not write to file: ", path, " (", strerror(errno), ")");
343 return SuccessResult;
347 Result_t Path$write(Path_t path, Text_t text, int permissions) {
348 List_t bytes = Text$utf8(text);
349 return _write(path, bytes, O_WRONLY | O_CREAT | O_TRUNC, permissions);
353 Result_t Path$write_bytes(Path_t path, List_t bytes, int permissions) {
354 return _write(path, bytes, O_WRONLY | O_CREAT | O_TRUNC, permissions);
358 Result_t Path$append(Path_t path, Text_t text, int permissions) {
359 List_t bytes = Text$utf8(text);
360 return _write(path, bytes, O_WRONLY | O_APPEND | O_CREAT, permissions);
364 Result_t Path$append_bytes(Path_t path, List_t bytes, int permissions) {
365 return _write(path, bytes, O_WRONLY | O_APPEND | O_CREAT, permissions);
375 static Result_t _write_bytes_to_fd(List_t bytes, bool close_file, void *userdata) {
376 writer_data_t *data = userdata;
377 if (bytes.length > 0) {
378 if (data->fd == -1) {
379 data->fd = open(data->path, data->mode, data->permissions);
380 if (data->fd == -1) {
381 if (errno == EMFILE || errno == ENFILE) {
382 // If we hit file handle limits, run GC collection to try to clean up any lingering file handles
383 // that will be closed by GC finalizers.
385 data->fd = open(data->path, data->mode, data->permissions);
388 return FailureResult("Could not write to file: ", data->path, " (", strerror(errno), ")");
392 if (bytes.stride != 1) List$compact(&bytes, 1);
393 ssize_t written = write(data->fd, bytes.data, (size_t)bytes.length);
394 if (written != (ssize_t)bytes.length)
395 return FailureResult("Could not write to file: ", data->path, " (", strerror(errno), ")");
397 // After first successful write, all writes are appends
398 data->mode = (O_WRONLY | O_CREAT | O_APPEND);
400 if (close_file && data->fd != -1) {
401 if (close(data->fd) == -1)
402 return FailureResult("Failed to close file: ", data->path, " (", strerror(errno), ")");
405 return SuccessResult;
408 static Result_t _write_text_to_fd(Text_t text, bool close_file, void *userdata) {
409 return _write_bytes_to_fd(Text$utf8(text), close_file, userdata);
412 static void _writer_cleanup(writer_data_t *data) {
413 if (data && data->fd != -1) {
420 Closure_t Path$byte_writer(Path_t path, bool append, int permissions) {
421 path = Path$expand_home(path);
422 int mode = append ? (O_WRONLY | O_CREAT | O_APPEND) : (O_WRONLY | O_CREAT | O_TRUNC);
423 writer_data_t *userdata = new (writer_data_t, .fd = -1, .path = path, .mode = mode, .permissions = permissions);
424 GC_register_finalizer(userdata, (void *)_writer_cleanup, NULL, NULL, NULL);
425 return (Closure_t){.fn = _write_bytes_to_fd, .userdata = userdata};
429 Closure_t Path$writer(Path_t path, bool append, int permissions) {
430 path = Path$expand_home(path);
431 int mode = append ? (O_WRONLY | O_CREAT | O_APPEND) : (O_WRONLY | O_CREAT | O_TRUNC);
432 writer_data_t *userdata = new (writer_data_t, .fd = -1, .path = path, .mode = mode, .permissions = permissions);
433 GC_register_finalizer(userdata, (void *)_writer_cleanup, NULL, NULL, NULL);
434 return (Closure_t){.fn = _write_text_to_fd, .userdata = userdata};
438 OptionalList_t Path$read_bytes(Path_t path, OptionalInt_t count) {
439 path = Path$expand_home(path);
440 int fd = open(path, O_RDONLY);
442 if (errno == EMFILE || errno == ENFILE) {
443 // If we hit file handle limits, run GC collection to try to clean up any lingering file handles that
444 // will be closed by GC finalizers.
446 fd = open(path, O_RDONLY);
450 if (fd == -1) return NONE_LIST;
453 if (fstat(fd, &sb) != 0) return NONE_LIST;
455 int64_t const target_count = count.small ? Int64$from_int(count, false) : INT64_MAX;
456 if (target_count < 0) fail("Cannot read a negative number of bytes!");
458 if ((sb.st_mode & S_IFMT) == S_IFREG) { // Use memory mapping if it's a real file:
459 const char *mem = mmap(NULL, (size_t)sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
460 char *content = GC_MALLOC_ATOMIC((size_t)sb.st_size + 1);
461 memcpy(content, mem, (size_t)sb.st_size);
462 content[sb.st_size] = '\0';
464 if (count.small && (int64_t)sb.st_size < target_count) return NONE_LIST;
465 int64_t len = count.small ? target_count : (int64_t)sb.st_size;
466 return (List_t){.data = content, .atomic = 1, .stride = 1, .length = (uint64_t)len};
468 size_t capacity = 256, len = 0;
469 char *content = GC_MALLOC_ATOMIC(capacity);
470 int64_t count_remaining = target_count;
473 size_t to_read = count_remaining < (int64_t)sizeof(chunk) ? (size_t)count_remaining : sizeof(chunk);
474 ssize_t just_read = read(fd, chunk, to_read);
478 } else if (just_read == 0) {
479 if (errno == EAGAIN || errno == EINTR) continue;
482 count_remaining -= (int64_t)just_read;
484 if (len + (size_t)just_read >= capacity) {
485 content = GC_REALLOC(content, (capacity *= 2));
488 memcpy(&content[len], chunk, (size_t)just_read);
489 len += (size_t)just_read;
492 if (count.small != 0 && (int64_t)len < target_count) return NONE_LIST;
493 return (List_t){.data = content, .atomic = 1, .stride = 1, .length = (uint64_t)len};
498 OptionalText_t Path$read(Path_t path) {
499 List_t bytes = Path$read_bytes(path, NONE_INT);
500 if (bytes.data == NULL) return NONE_TEXT;
501 return Text$from_utf8(bytes);
505 OptionalText_t Path$owner(Path_t path, bool follow_symlinks) {
507 int status = path_stat(path, follow_symlinks, &sb);
508 if (status != 0) return NONE_TEXT;
509 struct passwd *pw = getpwuid(sb.st_uid);
510 return pw ? Text$from_str(pw->pw_name) : NONE_TEXT;
514 OptionalText_t Path$group(Path_t path, bool follow_symlinks) {
516 int status = path_stat(path, follow_symlinks, &sb);
517 if (status != 0) return NONE_TEXT;
518 struct group *gr = getgrgid(sb.st_uid);
519 return gr ? Text$from_str(gr->gr_name) : NONE_TEXT;
523 Result_t Path$set_owner(Path_t path, OptionalText_t owner, OptionalText_t group, bool follow_symlinks) {
524 uid_t owner_id = (uid_t)-1;
525 if (owner.tag == TEXT_NONE) {
526 struct passwd *pwd = getpwnam(Text$as_c_string(owner));
527 if (pwd == NULL) return FailureResult("Not a valid user: ", owner);
528 owner_id = pwd->pw_uid;
531 gid_t group_id = (gid_t)-1;
532 if (group.tag == TEXT_NONE) {
533 struct group *grp = getgrnam(Text$as_c_string(group));
534 if (grp == NULL) return FailureResult("Not a valid group: ", group);
535 group_id = grp->gr_gid;
537 int result = follow_symlinks ? chown(path, owner_id, group_id) : lchown(path, owner_id, group_id);
538 if (result < 0) return FailureResult("Could not set owner!");
539 return SuccessResult;
542 static int _remove_files(const char *path, const struct stat *sbuf, int type, struct FTW *ftwb) {
543 (void)sbuf, (void)ftwb;
548 if (remove(path) < 0) {
549 fail("Could not remove file: ", path, " (", strerror(errno), ")");
554 if (rmdir(path) != 0) fail("Could not remove directory: ", path, " (", strerror(errno), ")");
556 default: fail("Could not remove path: ", path, " (not a file or directory)"); return -1;
561 Result_t Path$remove(Path_t path, bool ignore_missing) {
562 path = Path$expand_home(path);
564 if (lstat(path, &sb) != 0) {
565 if (!ignore_missing) return FailureResult("Could not remove file: ", path, " (", strerror(errno), ")");
566 return SuccessResult;
569 if ((sb.st_mode & S_IFMT) == S_IFREG || (sb.st_mode & S_IFMT) == S_IFLNK) {
570 if (unlink(path) != 0 && !ignore_missing)
571 return FailureResult("Could not remove file: ", path, " (", strerror(errno), ")");
572 } else if ((sb.st_mode & S_IFMT) == S_IFDIR) {
573 const int num_open_fd = 10;
574 if (nftw(path, _remove_files, num_open_fd, FTW_DEPTH | FTW_MOUNT | FTW_PHYS) < 0)
575 return FailureResult("Could not remove directory: ", path, " (", strerror(errno), ")");
577 return FailureResult("Could not remove path: ", path, " (not a file or directory)");
579 return SuccessResult;
582 Result_t Path$move(Path_t src, Path_t dest, bool allow_overwriting) {
583 int status = rename(src, dest);
585 if (errno == EEXIST && allow_overwriting) {
586 Result_t result = Path$remove(dest, true);
587 if (result.Failure.reason.tag != TEXT_NONE) return result;
588 return Path$move(src, dest, allow_overwriting);
590 return FailureResult("Could not move file ", src, " to ", dest, " (", strerror(errno), ")");
592 return SuccessResult;
596 Result_t Path$create_directory(Path_t path, int permissions, bool recursive) {
598 path = Path$expand_home(path);
599 const char *c_path = Path$as_c_string(path);
600 int status = mkdir(c_path, (mode_t)permissions);
602 if (recursive && errno == ENOENT) {
603 Path$create_directory(Path$parent(path), permissions, recursive);
605 } else if (errno != EEXIST) {
606 return FailureResult("Could not create directory: ", c_path, " (", strerror(errno), ")");
609 return SuccessResult;
612 static OptionalList_t _filtered_children(Path_t path, bool include_hidden, mode_t filter) {
613 path = Path$resolved(path, Path$current_dir());
614 List_t children = EMPTY_LIST;
615 size_t path_len = strlen(path);
616 DIR *d = opendir(path);
617 if (!d) return NONE_LIST;
619 if (path[path_len - 1] == '/') --path_len;
622 while ((ent = readdir(d)) != NULL) {
623 if (!include_hidden && ent->d_name[0] == '.') continue;
624 if (streq(ent->d_name, ".") || streq(ent->d_name, "..")) continue;
626 const char *child_str = String(string_slice(path, path_len), "/", ent->d_name);
628 if (stat(child_str, &sb) != 0) continue;
629 if (!((sb.st_mode & S_IFMT) & filter)) continue;
631 Path_t child = Path$from_str(child_str);
632 List$insert(&children, &child, I(0), sizeof(Path_t));
639 OptionalList_t Path$children(Path_t path, bool include_hidden) {
640 return _filtered_children(path, include_hidden, (mode_t)-1);
644 OptionalList_t Path$files(Path_t path, bool include_hidden) {
645 return _filtered_children(path, include_hidden, S_IFREG);
649 OptionalList_t Path$subdirectories(Path_t path, bool include_hidden) {
650 return _filtered_children(path, include_hidden, S_IFDIR);
656 bool include_hidden : 1;
659 static OptionalPath_t _next_child(child_info_t *info) {
660 if (!info->dir) return NONE_PATH;
661 for (struct dirent *ent; (ent = readdir(info->dir)) != NULL;) {
662 if (!info->include_hidden && ent->d_name[0] == '.') continue;
663 if (streq(ent->d_name, ".") || streq(ent->d_name, "..")) continue;
665 Path_t child = Path$_concat2(info->path, ent->d_name);
674 Closure_t Path$each_child(Path_t path, bool include_hidden) {
675 path = Path$resolved(path, Path$current_dir());
677 DIR *d = opendir(path);
678 if (!d) return NONE_CLOSURE;
680 child_info_t *info = GC_malloc(sizeof(child_info_t));
683 info->include_hidden = include_hidden;
684 return (Closure_t){.fn = (void *)_next_child, .userdata = info};
688 OptionalPath_t Path$unique_directory(Path_t path) {
689 path = Path$expand_home(path);
690 size_t len = strlen(path);
691 if (len >= PATH_MAX) fail("Path is too long: ", path);
692 static char buf[PATH_MAX] = {};
693 memcpy(buf, path, len);
695 if (buf[len - 1] == '/') buf[--len] = '\0';
696 char *created = mkdtemp(buf);
697 if (!created) return NULL;
698 return Path$from_str(created);
702 OptionalPath_t Path$write_unique_bytes(Path_t path, List_t bytes) {
703 path = Path$expand_home(path);
704 size_t len = strlen(path);
705 if (len >= PATH_MAX) fail("Path is too long: ", path);
706 static char buf[PATH_MAX] = {};
707 memcpy(buf, path, len);
710 // Count the number of trailing characters leading up to the last "X"
711 // (e.g. "foo_XXXXXX.tmp" would yield suffixlen = 4)
712 size_t suffixlen = 0;
713 while (suffixlen < len && buf[len - 1 - suffixlen] != 'X')
716 int fd = mkstemps(buf, suffixlen);
717 if (fd == -1) return NULL;
719 if (bytes.stride != 1) List$compact(&bytes, 1);
721 ssize_t written = write(fd, bytes.data, (size_t)bytes.length);
722 if (written != (ssize_t)bytes.length) fail("Could not write to file: ", buf, " (", strerror(errno), ")");
724 return Path$from_str(buf);
728 OptionalPath_t Path$write_unique(Path_t path, Text_t text) {
729 return Path$write_unique_bytes(path, Text$utf8(text));
733 OptionalPath_t Path$parent(Path_t path) {
734 if (!path || path[0] == '\0' || strspn(path, "/") == strlen(path)) {
735 // root dir has no parent
738 if (streq(path, ".")) return PARENT_PATH;
739 static char buf[PATH_MAX];
740 snprintf(buf, sizeof(buf), "%s/..", path);
741 return path_from_buf(buf);
744 static const char *base_name_start(Path_t path) {
745 if (!path || path[0] == '\0') return "";
747 const char *end = path + strlen(path);
748 // Strip trailing slash
749 while (end > path && end[0] == '/')
752 // Get component up to end, excluding trailing slash
753 while (end > path && end[-1] != '/')
760 PUREFUNC Text_t Path$base_name(Path_t path) {
761 const char *base = base_name_start(path);
762 return Text$from_strn(base, strcspn(base, "/"));
766 Text_t Path$extension(Path_t path, bool full) {
767 const char *base = base_name_start(path);
768 if (!base || base[0] == '\0') return EMPTY_TEXT;
769 if (base[0] == '.') base += 1;
770 const char *dot = full ? strchr(base + 1, '.') : strrchr(base + 1, '.');
771 const char *extension = dot ? dot + 1 : "";
772 return Text$from_strn(extension, strcspn(extension, "/"));
776 bool Path$has_extension(Path_t path, Text_t extension) {
777 const char *base = base_name_start(path);
778 if (!base || base[0] == '\0') return false;
779 if (base[0] == '.') base += 1;
780 const char *end = base;
781 while (*end && *end != '/')
783 int64_t base_len = (int64_t)(end - base);
784 if (base_len <= 0) return false;
785 if (extension.length == 0) {
786 const char *dot = strrchr(base, '.');
787 return dot == NULL || dot[1] == '\0' || dot == base;
789 const char *ext = Text$as_c_string(extension);
791 if (1 + (int64_t)extension.length > base_len) return false;
792 return strncmp(base + base_len - extension.length, ext, extension.length) == 0;
794 if (1 + 1 + (int64_t)extension.length > base_len) return false;
795 return base[base_len - 1 - extension.length] == '.'
796 && strncmp(base + base_len - extension.length, ext, extension.length) == 0;
801 Path_t Path$child(Path_t path, Text_t name) {
802 static char buf[PATH_MAX];
803 snprintf(buf, sizeof(buf), "%s/%s", path, Text$as_c_string(name));
804 return path_from_buf(buf);
808 Path_t Path$sibling(Path_t path, Text_t name) {
809 static char buf[PATH_MAX];
810 snprintf(buf, sizeof(buf), "%s/../%s", path, Text$as_c_string(name));
811 return path_from_buf(buf);
815 OptionalPath_t Path$with_extension(Path_t path, Text_t extension, bool replace) {
816 if (!path || path[0] == '\0') return NULL;
818 static char buf[PATH_MAX];
819 const char *ext = Text$as_c_string(extension);
821 char *base = (char *)base_name_start(path);
823 while (*dot && *dot != '.')
825 if (ext[0] == '.' || ext[0] == '\0') snprintf(buf, sizeof(buf), "%.*s%s", (int)(dot - path), path, ext);
826 else snprintf(buf, sizeof(buf), "%.*s.%s", (int)(dot - path), path, ext);
828 if (ext[0] == '.' || ext[0] == '\0') snprintf(buf, sizeof(buf), "%s%s", path, ext);
829 else snprintf(buf, sizeof(buf), "%s.%s", path, ext);
831 return path_from_buf(buf);
834 static void _line_reader_cleanup(FILE **f) {
841 static Text_t _next_line(FILE **f) {
842 if (!f || !*f) return NONE_TEXT;
847 ssize_t len = getline(&line, &size, *f);
849 if (line != NULL) free(line);
850 _line_reader_cleanup(f);
854 while (len > 0 && (line[len - 1] == '\r' || line[len - 1] == '\n'))
857 if (u8_check((uint8_t *)line, (size_t)len) != NULL) {
858 // If there's invalid UTF8, skip this line and move to the next
862 Text_t line_text = Text$from_strn(line, (size_t)len);
868 OptionalClosure_t Path$by_line(Path_t path) {
869 path = Path$expand_home(path);
871 FILE *f = fopen(path, "r");
873 if (errno == EMFILE || errno == ENFILE) {
874 // If we hit file handle limits, run GC collection to try to clean up any lingering file handles that
875 // will be closed by GC finalizers.
877 f = fopen(path, "r");
881 if (f == NULL) return NONE_CLOSURE;
883 FILE **wrapper = GC_MALLOC(sizeof(FILE *));
885 GC_register_finalizer(wrapper, (void *)_line_reader_cleanup, NULL, NULL, NULL);
886 return (Closure_t){.fn = (void *)_next_line, .userdata = wrapper};
890 OptionalList_t Path$lines(Path_t path) {
891 FILE *f = fopen(path, "r");
893 if (errno == EMFILE || errno == ENFILE) {
894 // If we hit file handle limits, run GC collection to try to clean up any lingering file handles that
895 // will be closed by GC finalizers.
897 f = fopen(path, "r");
901 if (f == NULL) return NONE_LIST;
903 List_t lines = EMPTY_LIST;
904 for (OptionalText_t line; (line = _next_line(&f)).tag != TEXT_NONE;) {
905 List$insert(&lines, &line, I(0), sizeof(line));
911 List_t Path$glob(Path_t path) {
913 int status = glob(Path$as_c_string(path), GLOB_BRACE | GLOB_TILDE, NULL, &glob_result);
914 if (status != 0 && status != GLOB_NOMATCH) fail("Failed to perform globbing");
916 List_t glob_files = EMPTY_LIST;
917 for (size_t i = 0; i < glob_result.gl_pathc; i++) {
918 size_t len = strlen(glob_result.gl_pathv[i]);
919 if ((len >= 2 && glob_result.gl_pathv[i][len - 1] == '.' && glob_result.gl_pathv[i][len - 2] == '/')
920 || (len >= 2 && glob_result.gl_pathv[i][len - 1] == '.' && glob_result.gl_pathv[i][len - 2] == '.'
921 && glob_result.gl_pathv[i][len - 3] == '/'))
923 Path_t p = Path$from_str(glob_result.gl_pathv[i]);
924 List$insert(&glob_files, &p, I(0), sizeof(Path_t));
930 bool Path$matches_glob(Path_t path, Text_t glob) {
931 return !fnmatch(Text$as_c_string(glob), path, FNM_PATHNAME | FNM_PERIOD);
935 Path_t Path$current_dir(void) {
936 static char cwd[PATH_MAX];
937 if (getcwd(cwd, sizeof(cwd)) == NULL) fail("Could not get current working directory");
938 return Path$from_str(cwd);
943 OptionalPath_t current;
945 bool include_hidden : 1, follow_symlinks : 1;
948 static OptionalPath_t _walk_next_path(walk_info_t *info) {
949 while (info->dir == NULL) {
950 if (info->dir_stack.length == 0) return NONE_PATH;
952 Path_t p = *(Path_t *)info->dir_stack.data;
953 List$remove_at(&info->dir_stack, I(1), I(1), sizeof(Path_t));
954 info->dir = opendir(p);
959 for (struct dirent *ent; (ent = readdir(info->dir)) != NULL;) {
960 if (!info->include_hidden && ent->d_name[0] == '.') continue;
961 if (streq(ent->d_name, ".") || streq(ent->d_name, "..")) continue;
963 Path_t path = Path$_concat2(info->current, Path$from_str(ent->d_name));
964 if (Path$is_directory(path, info->follow_symlinks)) {
965 List$insert(&info->dir_stack, &path, I(0), sizeof(Path_t));
973 return _walk_next_path(info);
977 Closure_t Path$walk(Path_t dir, bool include_hidden, bool follow_symlinks) {
978 dir = Path$resolved(dir, Path$current_dir());
979 walk_info_t *info = GC_malloc(sizeof(walk_info_t));
980 info->dir_stack = List(dir);
983 info->include_hidden = include_hidden;
984 info->follow_symlinks = follow_symlinks;
985 return (Closure_t){.fn = (void *)_walk_next_path, .userdata = info};
990 const char *Path$as_c_string(Path_t path) {
995 Text_t Path$as_text(const void *obj, bool color, const TypeInfo_t *type) {
997 if (!obj) return Text("Path");
998 Path_t *path = (Path_t *)obj;
999 Text_t text = Text$from_str(*path);
1000 if (color) text = Text$concat(Text("\033[32;1m"), text, Text("\033[m"));
1005 const TypeInfo_t Path$info = {
1006 .size = sizeof(Path_t),
1007 .align = __alignof__(Path_t),
1011 .as_text = Path$as_text,
1012 .compare = CString$compare,
1013 .equal = CString$equal,
1014 .hash = CString$hash,
1015 .is_none = CString$is_none,
1016 .serialize = CString$serialize,
1017 .deserialize = CString$deserialize,