diff --git a/Makefile b/Makefile index 1637e9e2..2a0d0ead 100644 --- a/Makefile +++ b/Makefile @@ -50,7 +50,7 @@ else endif ifneq (,$(findstring wasm32,$(TRIPLE))) - RUNNER = wasmtime run + RUNNER = wasmtime run --dir $(CURDIR) endif LDLIBS_PLATFORM = @@ -110,6 +110,12 @@ endif TESTS = amalg app array asset cli etc env format fmon glob ht io math process ps rb str sys thread time mem prompt leak qsort term BENCHES = glob heap EXAMPLES = app array cargo cli format hash_table io zero_copy ls palette post prompt prompt_fancy serve signal tls wc +EXAMPLES += fs/config fs/iterator fs/ls fs/roots fs/workspace +ifneq (,$(findstring wasm32,$(TRIPLE))) + EXAMPLES += fs/preopen +else + EXAMPLES += fs/absolute +endif TRIPLES = \ x86_64-linux-none x86_64-linux-gnu x86_64-linux-musl \ aarch64-linux-none aarch64-linux-gnu aarch64-linux-musl \ @@ -148,7 +154,7 @@ $(EXAMPLE_DIR)/post$(EXE): example/post.c $(SP_HEADERS) $(MBEDTLS_LIB) | $(EXAMP endif -$(EXAMPLE_DIR)/%$(EXE): example/%.c $(SP_HEADERS) | $(EXAMPLE_DIR) +$(EXAMPLE_DIR)/%$(EXE): example/%.c $(SP_HEADERS) | $(EXAMPLE_DIR) $(EXAMPLE_DIR)/fs $(CC) $(CFLAGS) -I. -o $@ $< $(LDLIBS_PLATFORM) $(TEST_DIR)/%$(EXE): test/%.c $(SP_HEADERS) $(TEST_SOURCES) | $(TEST_DIR) @@ -178,7 +184,7 @@ wasm: +$(MAKE) wasm32-wasi wasm32-freestanding +$(MAKE) MODE=cpp wasm32-wasi wasm32-freestanding -$(BUILD_DIR) $(EXAMPLE_DIR) $(TEST_DIR) $(BENCH_DIR) $(BUILD_DIR)/mbedtls: +$(BUILD_DIR) $(EXAMPLE_DIR) $(EXAMPLE_DIR)/fs $(TEST_DIR) $(BENCH_DIR) $(BUILD_DIR)/mbedtls: mkdir -p $@ clean: diff --git a/README.md b/README.md index 831c4a2e..29bf8719 100644 --- a/README.md +++ b/README.md @@ -36,13 +36,16 @@ s32 compare_entries(const void* pa, const void* pb) { return sp_str_compare_alphabetical(a->name, b->name); } -s32 main(s32 num_args, const c8** args) { - sp_mem_t mem = sp_mem_os_new(); - sp_str_t cwd = sp_fs_get_cwd(mem); - sp_str_t dir = cwd; - if (num_args == 2) dir = sp_fs_join_path(mem, cwd, sp_cstr_as_str(args[1])); - - sp_da(sp_fs_entry_t) entries = sp_fs_collect(mem, dir); +s32 run(s32 num_args, const c8** args) { + sp_mem_heap_t* heap = sp_mem_heap_new(); + sp_mem_t mem = sp_mem_heap_as_allocator(heap); + sp_str_t dir = num_args == 2 ? sp_cstr_as_str(args[1]) : sp_str_lit("."); + + sp_da(sp_fs_entry_t) entries; + if (sp_fs_collect(mem, dir, &entries)) { + sp_log("could not read {}", sp_fmt_str(dir)); + return 1; + } sp_da_sort(entries, compare_entries); sp_da_for(entries, it) { @@ -56,6 +59,7 @@ s32 main(s32 num_args, const c8** args) { } return 0; } +SP_MAIN(run) ``` A few modules showcased in this example: - `sp_mem_t` is an allocator; everything that allocates takes one. In the example, we use the default heap allocator. diff --git a/example/fs/absolute.c b/example/fs/absolute.c new file mode 100644 index 00000000..bf10c5bb --- /dev/null +++ b/example/fs/absolute.c @@ -0,0 +1,29 @@ +#define SP_IMPLEMENTATION +#include "sp.h" + +static sp_err_t save(sp_mem_t mem, sp_str_t file, sp_str_t* settings) { + sp_try(sp_fs_create_dir(sp_fs_parent_path(file))); + sp_try(sp_fs_write_atomic(file, sp_str_lit("theme = \"dark\"\n"))); + return sp_io_read_file(mem, file, settings); +} + +s32 run(s32 num_args, const c8** args) { + sp_mem_arena_marker_t s = sp_mem_begin_scratch(); + + sp_str_t config = sp_zero; + sp_str_t settings = sp_zero; + sp_err_t err = sp_fs_get_config_path(s.mem, &config); + sp_str_t file = sp_fs_join_path(s.mem, config, sp_str_lit("sp/settings.toml")); + if (!err) err = save(s.mem, file, &settings); + if (err) { + sp_log("{.red}: {}", sp_fmt_str(file), sp_fmt_str(sp_err_str(err))); + } + else { + sp_log("{.cyan}", sp_fmt_str(file)); + sp_print("{}", sp_fmt_str(settings)); + } + + sp_mem_end_scratch(s); + return err ? 1 : 0; +} +SP_MAIN(run) diff --git a/example/fs/config.c b/example/fs/config.c new file mode 100644 index 00000000..2cfc094f --- /dev/null +++ b/example/fs/config.c @@ -0,0 +1,27 @@ +#define SP_IMPLEMENTATION +#include "sp.h" + +static sp_err_t load(sp_mem_t mem, sp_str_t path, sp_str_t* config) { + if (!sp_fs_exists(path)) { + sp_try(sp_fs_create_file_str(path, sp_str_lit("name = \"sp\"\n"))); + } + return sp_io_read_file(mem, path, config); +} + +s32 run(s32 num_args, const c8** args) { + sp_mem_arena_marker_t s = sp_mem_begin_scratch(); + + sp_str_t path = sp_str_lit("app.toml"); + sp_str_t config = sp_zero; + sp_err_t err = load(s.mem, path, &config); + if (err) { + sp_log("{.red}: {}", sp_fmt_str(path), sp_fmt_str(sp_err_str(err))); + } + else { + sp_print("{}", sp_fmt_str(config)); + } + + sp_mem_end_scratch(s); + return err ? 1 : 0; +} +SP_MAIN(run) diff --git a/example/fs/iterator.c b/example/fs/iterator.c new file mode 100644 index 00000000..e0a4f6d1 --- /dev/null +++ b/example/fs/iterator.c @@ -0,0 +1,64 @@ +#define SP_IMPLEMENTATION +#include "sp.h" + +static bool is_hidden(sp_fs_entry_t entry) { + return sp_str_starts_with(entry.name, sp_str_lit(".")); +} + +static void print_entry(sp_fs_entry_t entry, u32 depth) { + sp_print("{:$}", sp_fmt_uint(depth * 2), sp_fmt_cstr("")); + switch (entry.kind) { + case SP_FS_KIND_DIR: { + sp_log("{.blue}/", sp_fmt_str(entry.name)); + break; + } + case SP_FS_KIND_FILE: { + sp_log("{}", sp_fmt_str(entry.name)); + break; + } + case SP_FS_KIND_SYMLINK: { + sp_log("{.cyan}", sp_fmt_str(entry.name)); + break; + } + case SP_FS_KIND_NONE: { + break; + } + } +} + +s32 run(s32 num_args, const c8** args) { + sp_str_t dir = num_args > 1 ? sp_cstr_as_str(args[1]) : sp_str_lit("."); + + sp_mem_arena_marker_t s = sp_mem_begin_scratch(); + sp_fs_it_t it = sp_fs_it_new_at(s.mem, sp_path_from_str(dir), 0); + sp_err_t err = it.err; + u32 dirs = 0; + while (!err && sp_fs_it_next(&it)) { + switch (it.yield) { + case SP_FS_IT_ENTRY: { + if (is_hidden(it.entry)) break; + + print_entry(it.entry, sp_da_size(it.stack) - 1); + if (it.entry.kind == SP_FS_KIND_DIR) err = sp_fs_it_enter(&it); + break; + } + case SP_FS_IT_LEAVE: { + dirs++; + break; + } + } + } + if (!err) err = it.err; + + if (err) { + sp_log("{.red}: {}", sp_fmt_str(dir), sp_fmt_str(sp_err_str(err))); + } + else { + sp_log("{} {.gray}", sp_fmt_uint(dirs), sp_fmt_cstr("directories")); + } + + sp_fs_it_deinit(&it); + sp_mem_end_scratch(s); + return err ? 1 : 0; +} +SP_MAIN(run) diff --git a/example/fs/ls.c b/example/fs/ls.c new file mode 100644 index 00000000..87e3f45f --- /dev/null +++ b/example/fs/ls.c @@ -0,0 +1,51 @@ +#define SP_IMPLEMENTATION +#include "sp.h" + +static void print_entry(sp_mem_t mem, sp_fs_it_t* it) { + sp_sys_file_meta_t meta = sp_zero; + sp_sys_get_link_metadata_s(it->at.dir, it->at.sub, &meta); + + sp_tm_epoch_t modified = { .s = (u64)meta.mtime.tv_sec, .ns = (u32)meta.mtime.tv_nsec }; + sp_print("{:>10} {.gray} ", sp_fmt_int(meta.size), sp_fmt_str(sp_tm_epoch_to_iso8601(mem, modified))); + + switch (it->entry.kind) { + case SP_FS_KIND_DIR: { + sp_log("{.blue}", sp_fmt_str(it->entry.name)); + break; + } + case SP_FS_KIND_FILE: { + sp_log("{}", sp_fmt_str(it->entry.name)); + break; + } + case SP_FS_KIND_SYMLINK: { + c8 buf [SP_PATH_MAX]; + sp_str_t target = sp_zero; + sp_sys_readlink_s(it->at.dir, it->at.sub, buf, sizeof(buf), &target); + sp_log("{.cyan} -> {}", sp_fmt_str(it->entry.name), sp_fmt_str(target)); + break; + } + case SP_FS_KIND_NONE: { + break; + } + } +} + +s32 run(s32 num_args, const c8** args) { + sp_str_t dir = num_args > 1 ? sp_cstr_as_str(args[1]) : sp_str_lit("."); + + sp_mem_arena_marker_t s = sp_mem_begin_scratch(); + sp_fs_it_t it = sp_fs_it_new_at(s.mem, sp_path_from_str(dir), 0); + while (sp_fs_it_next(&it)) { + print_entry(s.mem, &it); + } + + sp_err_t err = it.err; + if (err) { + sp_log("{.red}: {}", sp_fmt_str(dir), sp_fmt_str(sp_err_str(err))); + } + + sp_fs_it_deinit(&it); + sp_mem_end_scratch(s); + return err ? 1 : 0; +} +SP_MAIN(run) diff --git a/example/fs/preopen.c b/example/fs/preopen.c new file mode 100644 index 00000000..3b83f9fb --- /dev/null +++ b/example/fs/preopen.c @@ -0,0 +1,25 @@ +#define SP_IMPLEMENTATION +#include "sp.h" + +static sp_err_t start(sp_mem_t mem, sp_path_t config, sp_path_t state) { + sp_str_t content = sp_zero; + sp_try(sp_io_read_file_at(mem, config, &content)); + sp_print("{}", sp_fmt_str(content)); + + return sp_fs_write_atomic_at(state, sp_tm_epoch_to_iso8601(mem, sp_tm_now_epoch())); +} + +s32 run(s32 num_args, const c8** args) { + sp_mem_arena_marker_t s = sp_mem_begin_scratch(); + + sp_path_t config = sp_path_from_str(sp_str_lit("/config/app.toml")); + sp_path_t state = sp_path_from_str(sp_str_lit("/data/last_run")); + sp_err_t err = start(s.mem, config, state); + if (err) { + sp_log("{.red}", sp_fmt_str(sp_err_str(err))); + } + + sp_mem_end_scratch(s); + return err ? 1 : 0; +} +SP_MAIN(run) diff --git a/example/fs/roots.c b/example/fs/roots.c new file mode 100644 index 00000000..1f0811a4 --- /dev/null +++ b/example/fs/roots.c @@ -0,0 +1,37 @@ +#define SP_IMPLEMENTATION +#include "sp.h" + +static void print_root(s32 it) { + c8 buf [SP_PATH_MAX]; + u64 len = 0; + sp_err_t err = sp_sys_get_root_label(it, buf, sizeof(buf), &len); + if (err) { + sp_log("{:>4} {.red}", sp_fmt_int(sp_sys_get_root(it)), sp_fmt_str(sp_err_str(err))); + } + else { + sp_log("{:>4} {.cyan}", sp_fmt_int(sp_sys_get_root(it)), sp_fmt_str(sp_str(buf, (u32)len))); + } +} + +static void print_path(sp_str_t path) { + sp_path_t resolved = sp_path_from_str(path); + sp_log( + "{:>4} {} {.gray} {}", + sp_fmt_int(resolved.dir), + sp_fmt_str(resolved.sub), + sp_fmt_cstr("<-"), + sp_fmt_str(path) + ); +} + +s32 run(s32 num_args, const c8** args) { + for (s32 it = 0; sp_sys_get_root(it) != SP_SYS_INVALID_FD; it++) { + print_root(it); + } + + sp_for_range(it, 1, (u32)num_args) { + print_path(sp_cstr_as_str(args[it])); + } + return 0; +} +SP_MAIN(run) diff --git a/example/fs/workspace.c b/example/fs/workspace.c new file mode 100644 index 00000000..8ade0591 --- /dev/null +++ b/example/fs/workspace.c @@ -0,0 +1,49 @@ +#define SP_IMPLEMENTATION +#include "sp.h" + +static sp_err_t list(sp_sys_fd_t dir) { + sp_mem_arena_marker_t s = sp_mem_begin_scratch(); + sp_fs_it_t it = sp_fs_it_new_at(s.mem, sp_path(dir, sp_str_lit(".")), 0); + while (sp_fs_it_walk(&it)) { + sp_log("{}", sp_fmt_str(it.entry.rel)); + } + + sp_err_t err = it.err; + sp_fs_it_deinit(&it); + sp_mem_end_scratch(s); + return err; +} + +static sp_err_t build(sp_sys_fd_t dir) { + sp_try(sp_fs_create_dir_at(sp_path(dir, sp_str_lit("src/nested")))); + sp_try(sp_fs_create_file_cstr_at(sp_path(dir, sp_str_lit("src/A")), "A")); + sp_try(sp_fs_create_file_cstr_at(sp_path(dir, sp_str_lit("src/nested/B")), "B")); + sp_try(sp_fs_copy_at(sp_path(dir, sp_str_lit("src")), sp_path(dir, sp_str_lit("dst")), SP_FS_ATOMIC_REPLACE)); + sp_try(sp_fs_remove_dir_at(sp_path(dir, sp_str_lit("src")))); + return list(dir); +} + +static sp_err_t work(sp_path_t path) { + sp_try(sp_fs_create_dir_at(path)); + + sp_sys_fd_t dir = SP_SYS_INVALID_FD; + sp_err_t err = sp_fs_open_dir_at(path, &dir); + if (!err) { + err = build(dir); + sp_sys_close(dir); + } + + sp_err_t removed = sp_fs_remove_dir_at(path); + return err ? err : removed; +} + +s32 run(s32 num_args, const c8** args) { + sp_str_t name = num_args > 1 ? sp_cstr_as_str(args[1]) : sp_str_lit("workspace"); + + sp_err_t err = work(sp_path_from_str(name)); + if (err) { + sp_log("{.red}: {}", sp_fmt_str(name), sp_fmt_str(sp_err_str(err))); + } + return err ? 1 : 0; +} +SP_MAIN(run) diff --git a/example/io.c b/example/io.c index f16af49c..b0430bb9 100644 --- a/example/io.c +++ b/example/io.c @@ -11,7 +11,8 @@ s32 run(s32 num_args, const c8** args) { .data = sp_alloc_n(mem, u8, 64), .capacity = 64 }; - sp_str_t exe = sp_fs_get_exe_path(mem); + sp_str_t exe = sp_zero; + sp_fs_get_exe_path(mem, &exe); // sp_io provides utilities for opening a file from a path sp_io_file_reader_from_path(&r, exe); @@ -24,7 +25,7 @@ s32 run(s32 num_args, const c8** args) { // Or, if you already have a file descriptor, wrap it. sp_io will close file descriptors // for you automatically if you pass SP_IO_CLOSE_MODE_AUTO sp_sys_fd_t fd = SP_SYS_INVALID_FD; - sp_sys_open_s(sp_sys_get_root(0), exe, SP_SYS_OPEN_MODE_RO, 0, &fd); + sp_sys_open_s(sp_fs_get_cwd(), exe, SP_SYS_OPEN_MODE_RO, 0, &fd); sp_io_file_reader_from_file(&r, (sp_sys_fd_t)fd, SP_IO_CLOSE_MODE_AUTO); sp_io_read(&r.base, buffer.data, buffer.capacity, &buffer.len); sp_log("sp_io_file_reader_from_file: {}", sp_fmt_str(sp_mem_buffer_as_str(&buffer))); diff --git a/example/ls.c b/example/ls.c index 203210cc..51f4ac68 100644 --- a/example/ls.c +++ b/example/ls.c @@ -10,9 +10,7 @@ s32 compare_entries(const void* pa, const void* pb) { s32 run(s32 num_args, const c8** args) { sp_mem_heap_t* heap = sp_mem_heap_new(); sp_mem_t mem = sp_mem_heap_as_allocator(heap); - sp_str_t cwd = sp_fs_get_cwd(mem); - sp_str_t dir = cwd; - if (num_args == 2) dir = sp_fs_join_path(mem, cwd, sp_str_view(args[1])); + sp_str_t dir = num_args == 2 ? sp_cstr_as_str(args[1]) : sp_str_lit("."); sp_da(sp_fs_entry_t) entries; if (sp_fs_collect(mem, dir, &entries)) { diff --git a/example/wc.c b/example/wc.c index 0457231a..f4a9b3a9 100644 --- a/example/wc.c +++ b/example/wc.c @@ -10,8 +10,7 @@ s32 run(s32 num_args, const c8** args) { sp_mem_heap_t* heap = sp_mem_heap_new(); sp_mem_t mem = sp_mem_heap_as_allocator(heap); - sp_str_t cwd = sp_fs_get_cwd(mem); - sp_str_t path = sp_fs_join_path(mem, cwd, sp_str_view(args[1])); + sp_str_t path = sp_cstr_as_str(args[1]); sp_str_t content = sp_zero; sp_io_read_file(mem, path, &content); diff --git a/example/zero_copy.c b/example/zero_copy.c index edcd22b5..d514726f 100644 --- a/example/zero_copy.c +++ b/example/zero_copy.c @@ -78,7 +78,7 @@ static run_t copy_run(sp_str_t src_path, sp_str_t dst_path, src_t src, sink_t si } case SRC_STREAM_FILE: { sp_sys_fd_t fd = SP_SYS_INVALID_FD; - if (sp_sys_open_s(sp_sys_get_root(0), src_path, SP_SYS_OPEN_MODE_RO, 0, &fd) != SP_OK) { + if (sp_sys_open_s(sp_fs_get_cwd(), src_path, SP_SYS_OPEN_MODE_RO, 0, &fd) != SP_OK) { return sp_zero_s(run_t); } sp_io_stream_reader_from_file(&sr, fd, SP_IO_CLOSE_MODE_AUTO); @@ -176,9 +176,8 @@ s32 run(s32 num_args, const c8** args) { sp_mem_heap_t* heap = sp_mem_heap_new(); sp_mem_t mem = sp_mem_heap_as_allocator(heap); - sp_str_t cwd = sp_fs_get_cwd(mem); - sp_str_t src = sp_str_concat(mem, cwd, sp_str_lit("/io.src")); - sp_str_t dst = sp_str_concat(mem, cwd, sp_str_lit("/io.dst")); + sp_str_t src = sp_str_lit("io.src"); + sp_str_t dst = sp_str_lit("io.dst"); u64 size_bytes = (u64)PERF_FILE_SIZE_MB * 1024u * 1024u; sp_log("preparing {} MiB source at {}", sp_fmt_uint(PERF_FILE_SIZE_MB), sp_fmt_str(src)); diff --git a/sp.h b/sp.h index 4d4375cd..4f03e592 100644 --- a/sp.h +++ b/sp.h @@ -942,6 +942,7 @@ typedef OVERLAPPED sp_win32_overlapped_t; #define SP_EROFS 30 #define SP_EMLINK 31 #define SP_EPIPE 32 + #define SP_ERANGE 34 #define SP_ENAMETOOLONG 36 #define SP_ENOSYS 38 #define SP_ENOTEMPTY 39 @@ -1080,6 +1081,7 @@ typedef OVERLAPPED sp_win32_overlapped_t; #define SP_EROFS EROFS #define SP_EMLINK EMLINK #define SP_EPIPE EPIPE + #define SP_ERANGE ERANGE #define SP_ENAMETOOLONG ENAMETOOLONG #define SP_ENOSYS ENOSYS #define SP_ENOTEMPTY ENOTEMPTY @@ -1387,9 +1389,9 @@ typedef struct { } sp_sys_file_meta_t; typedef struct { - s64 handle; - u64 cookie; -} sp_sys_dir_t; + sp_sys_fd_t fd; + s64 state; +} sp_sys_dir_it_t; typedef struct { const c8* name; @@ -1403,6 +1405,7 @@ typedef enum { SP_SYS_OPEN_MODE_RO, SP_SYS_OPEN_MODE_WO, SP_SYS_OPEN_MODE_RW, + SP_SYS_OPEN_MODE_PATH, } sp_sys_open_mode_t; typedef enum { @@ -1412,6 +1415,10 @@ typedef enum { SP_SYS_OPEN_APPEND = 1 << 3, } sp_sys_open_flags_t; +typedef enum { + SP_SYS_OPEN_DIR_NO_FOLLOW = 1 << 0, +} sp_sys_open_dir_flags_t; + typedef enum { SP_SYS_BLOCKING, SP_SYS_NONBLOCKING, @@ -1449,12 +1456,13 @@ SP_API sp_err_t sp_sys_pwrite(sp_sys_fd_t fd, const void* buf, u64 count, u64 SP_API sp_err_t sp_sys_transfer(sp_sys_fd_t in, u64* in_pos, sp_sys_fd_t out, u64 count, u64* bytes_moved); SP_API sp_err_t sp_sys_transfer_positional(sp_sys_fd_t in, u64* in_pos, sp_sys_fd_t out, u64 count, u64 offset, u64* bytes_moved); SP_API sp_sys_fd_t sp_sys_get_root(s32 it); -SP_API s64 sp_sys_get_exe_path(c8* buf, u64 size); -SP_API s64 sp_sys_get_cwd_path(c8* buf, u64 size); -SP_API s64 sp_sys_get_storage_path(c8* buf, u64 size); -SP_API s64 sp_sys_get_config_path(c8* buf, u64 size); +SP_API sp_err_t sp_sys_get_root_label(s32 it, c8* buf, u64 size, u64* len); +SP_API sp_err_t sp_sys_get_exe_path(c8* buf, u64 size, u64* len); +SP_API sp_err_t sp_sys_get_storage_path(c8* buf, u64 size, u64* len); +SP_API sp_err_t sp_sys_get_config_path(c8* buf, u64 size, u64* len); +SP_API sp_err_t sp_sys_get_fd_path(sp_sys_fd_t fd, c8* buf, u64 size, u64* len); SP_API sp_err_t sp_sys_open(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_open_mode_t mode, u32 flags, sp_sys_fd_t* out); -SP_API sp_err_t sp_sys_open_dir(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_fd_t* out); +SP_API sp_err_t sp_sys_open_dir(sp_sys_fd_t fd, const c8* path, u32 len, u32 flags, sp_sys_fd_t* out); SP_API sp_err_t sp_sys_close(sp_sys_fd_t fd); SP_API sp_err_t sp_sys_pipe(sp_sys_pipe_t* pipe, sp_sys_pipe_desc_t desc); SP_API sp_err_t sp_sys_pipe_ready(sp_sys_fd_t fd, u8* ready); @@ -1463,7 +1471,7 @@ SP_API sp_err_t sp_sys_rmdir(sp_sys_fd_t fd, const c8* path, u32 len); SP_API sp_err_t sp_sys_unlink(sp_sys_fd_t fd, const c8* path, u32 len); SP_API sp_err_t sp_sys_rename(sp_sys_fd_t from, const c8* pfrom, u32 lf, sp_sys_fd_t to, const c8* pto, u32 lt); SP_API sp_err_t sp_sys_link(sp_sys_fd_t from, const c8* pfrom, u32 lf, sp_sys_fd_t to, const c8* pto, u32 lt); -SP_API sp_err_t sp_sys_symlink(const c8* from, u32 lf, sp_sys_fd_t to, const c8* pto, u32 lt); +SP_API sp_err_t sp_sys_symlink(const c8* from, u32 lf, sp_sys_fd_t to, const c8* pto, u32 lt, sp_fs_kind_t kind); SP_API sp_err_t sp_sys_readlink(sp_sys_fd_t fd, const c8* path, u32 len, c8* buf, u64 size, u64* target_len); SP_API sp_err_t sp_sys_get_path_metadata(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_meta_t* st); SP_API sp_err_t sp_sys_get_link_metadata(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_meta_t* st); @@ -1475,7 +1483,6 @@ SP_API sp_err_t sp_sys_nanosleep(const sp_sys_timespec_t* req, sp_sys_timespe SP_API bool sp_sys_futex_wait(u32* addr, u32 expected, const sp_sys_timespec_t* timeout); SP_API void sp_sys_futex_wake(u32* addr); SP_API void sp_sys_futex_wake_all(u32* addr); -SP_API s64 sp_sys_canonicalize_path(const c8* path, u32 len, c8* buf, u64 size); SP_API sp_err_t sp_sys_event_open(sp_sys_event_t* out); SP_API sp_err_t sp_sys_event_signal(sp_sys_event_t event); SP_API sp_err_t sp_sys_event_clear(sp_sys_event_t event); @@ -1510,12 +1517,12 @@ SP_API void sp_sys_assert(bool cond); SP_API void sp_sys_exit(s32 code); SP_API void sp_sys_env(const c8** env, u32* len); -SP_API s64 sp_sys_lseek(sp_sys_fd_t fd, s64 offset, s32 whence); +SP_API sp_err_t sp_sys_lseek(sp_sys_fd_t fd, s64 offset, s32 whence, s64* position); SP_API sp_err_t sp_sys_chdir(const c8* path, u32 len); SP_API sp_err_t sp_sys_open_s(sp_sys_fd_t fd, sp_str_t path, sp_sys_open_mode_t mode, u32 flags, sp_sys_fd_t* out); -SP_API sp_err_t sp_sys_open_dir_s(sp_sys_fd_t fd, sp_str_t path, sp_sys_fd_t* out); +SP_API sp_err_t sp_sys_open_dir_s(sp_sys_fd_t fd, sp_str_t path, u32 flags, sp_sys_fd_t* out); SP_API sp_err_t sp_sys_get_path_metadata_s(sp_sys_fd_t fd, sp_str_t path, sp_sys_file_meta_t* st); SP_API sp_err_t sp_sys_get_link_metadata_s(sp_sys_fd_t fd, sp_str_t path, sp_sys_file_meta_t* st); SP_API sp_err_t sp_sys_mkdir_s(sp_sys_fd_t fd, sp_str_t path, sp_sys_file_perms_t perms); @@ -1524,15 +1531,14 @@ SP_API sp_err_t sp_sys_unlink_s(sp_sys_fd_t fd, sp_str_t path); SP_API sp_err_t sp_sys_rename_s(sp_sys_fd_t from_fd, sp_str_t from, sp_sys_fd_t to_fd, sp_str_t to); SP_API sp_err_t sp_sys_chdir_s(sp_str_t path); SP_API sp_err_t sp_sys_link_s(sp_sys_fd_t from_fd, sp_str_t existing, sp_sys_fd_t to_fd, sp_str_t alias); -SP_API sp_err_t sp_sys_symlink_s(sp_str_t existing, sp_sys_fd_t to_fd, sp_str_t alias); +SP_API sp_err_t sp_sys_symlink_s(sp_str_t existing, sp_sys_fd_t to_fd, sp_str_t alias, sp_fs_kind_t kind); SP_API sp_err_t sp_sys_readlink_s(sp_sys_fd_t fd, sp_str_t path, c8* buf, u64 size, sp_str_t* target); SP_API sp_err_t sp_sys_set_file_perms_s(sp_sys_fd_t fd, sp_str_t path, sp_sys_file_perms_t perms); SP_API sp_err_t sp_sys_set_times_s(sp_sys_fd_t fd, sp_str_t path, sp_sys_timespec_t atime, sp_sys_timespec_t mtime); -SP_API s64 sp_sys_canonicalize_path_s(sp_str_t path, c8* buf, u64 size); -SP_API sp_err_t sp_sys_dir_from_fd(sp_sys_fd_t fd, sp_sys_dir_t* out); -SP_API sp_err_t sp_sys_dir_read(sp_sys_dir_t* dir, sp_mem_buffer_t* buf); -SP_API sp_err_t sp_sys_dir_parse(sp_sys_dir_t* dir, sp_mem_buffer_t* buf, u64* cursor, sp_sys_dir_entry_t* out); -SP_API sp_err_t sp_sys_dir_close(sp_sys_dir_t* dir); +SP_API sp_err_t sp_sys_dir_it_open(sp_sys_fd_t fd, sp_sys_dir_it_t* out); +SP_API sp_err_t sp_sys_dir_it_read(sp_sys_dir_it_t* it, sp_mem_buffer_t* buf); +SP_API sp_err_t sp_sys_dir_it_parse(sp_sys_dir_it_t* it, sp_mem_buffer_t* buf, u64* cursor, sp_sys_dir_entry_t* out); +SP_API sp_err_t sp_sys_dir_it_close(sp_sys_dir_it_t* it); SP_API bool sp_sys_is_read_only(sp_sys_file_perms_t perms); SP_API void sp_sys_set_read_only(sp_sys_file_perms_t* perms, bool read_only); SP_API void sp_sys_set_executable(sp_sys_file_perms_t* perms, bool executable); @@ -1549,12 +1555,13 @@ typedef struct { sp_err_t (*transfer)(sp_sys_fd_t in, u64* in_pos, sp_sys_fd_t out, u64 count, u64* bytes_moved); sp_err_t (*transfer_positional)(sp_sys_fd_t in, u64* in_pos, sp_sys_fd_t out, u64 count, u64 offset, u64* bytes_moved); sp_sys_fd_t (*get_root)(s32 it); - s64 (*get_exe_path)(c8* buf, u64 size); - s64 (*get_cwd_path)(c8* buf, u64 size); - s64 (*get_storage_path)(c8* buf, u64 size); - s64 (*get_config_path)(c8* buf, u64 size); + sp_err_t (*get_root_label)(s32 it, c8* buf, u64 size, u64* len); + sp_err_t (*get_exe_path)(c8* buf, u64 size, u64* len); + sp_err_t (*get_storage_path)(c8* buf, u64 size, u64* len); + sp_err_t (*get_config_path)(c8* buf, u64 size, u64* len); + sp_err_t (*get_fd_path)(sp_sys_fd_t fd, c8* buf, u64 size, u64* len); sp_err_t (*open)(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_open_mode_t mode, u32 flags, sp_sys_fd_t* out); - sp_err_t (*open_dir)(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_fd_t*); + sp_err_t (*open_dir)(sp_sys_fd_t fd, const c8* path, u32 len, u32 flags, sp_sys_fd_t*); sp_err_t (*close)(sp_sys_fd_t fd); sp_err_t (*pipe)(sp_sys_pipe_t* pipe, sp_sys_pipe_desc_t desc); sp_err_t (*pipe_ready)(sp_sys_fd_t fd, u8* ready); @@ -1563,7 +1570,7 @@ typedef struct { sp_err_t (*unlink)(sp_sys_fd_t fd, const c8* path, u32 len); sp_err_t (*rename)(sp_sys_fd_t from_fd, const c8* from, u32 from_len, sp_sys_fd_t to_fd, const c8* to, u32 to_len); sp_err_t (*link)(sp_sys_fd_t from_fd, const c8* existing, u32 existing_len, sp_sys_fd_t to_fd, const c8* alias, u32 alias_len); - sp_err_t (*symlink)(const c8* existing, u32 existing_len, sp_sys_fd_t to_fd, const c8* alias, u32 alias_len); + sp_err_t (*symlink)(const c8* existing, u32 existing_len, sp_sys_fd_t to_fd, const c8* alias, u32 alias_len, sp_fs_kind_t kind); sp_err_t (*readlink)(sp_sys_fd_t fd, const c8* path, u32 len, c8* buf, u64 size, u64* target_len); sp_err_t (*get_path_metadata)(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_meta_t* st); sp_err_t (*get_link_metadata)(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_meta_t* st); @@ -1575,7 +1582,6 @@ typedef struct { bool (*futex_wait)(u32* addr, u32 expected, const sp_sys_timespec_t* timeout); void (*futex_wake)(u32* addr); void (*futex_wake_all)(u32* addr); - s64 (*canonicalize_path)(const c8* path, u32 len, c8* buf, u64 size); sp_err_t (*event_open)(sp_sys_event_t* out); sp_err_t (*event_signal)(sp_sys_event_t event); sp_err_t (*event_clear)(sp_sys_event_t event); @@ -1609,12 +1615,12 @@ typedef struct { void (*assert)(bool cond); void (*exit)(s32 code); void (*env)(const c8** env, u32* len); - s64 (*lseek)(sp_sys_fd_t fd, s64 offset, s32 whence); + sp_err_t (*lseek)(sp_sys_fd_t fd, s64 offset, s32 whence, s64* position); sp_err_t (*chdir)(const c8* path, u32 len); - sp_err_t (*dir_from_fd)(sp_sys_fd_t fd, sp_sys_dir_t* out); - sp_err_t (*dir_read)(sp_sys_dir_t* dir, sp_mem_buffer_t* buf); - sp_err_t (*dir_parse)(sp_sys_dir_t* dir, sp_mem_buffer_t* buf, u64* cursor, sp_sys_dir_entry_t* out); - sp_err_t (*dir_close)(sp_sys_dir_t* dir); + sp_err_t (*dir_it_open)(sp_sys_fd_t fd, sp_sys_dir_it_t* out); + sp_err_t (*dir_it_read)(sp_sys_dir_it_t* it, sp_mem_buffer_t* buf); + sp_err_t (*dir_it_parse)(sp_sys_dir_it_t* it, sp_mem_buffer_t* buf, u64* cursor, sp_sys_dir_entry_t* out); + sp_err_t (*dir_it_close)(sp_sys_dir_it_t* it); } sp_sys_vtable_t; SP_API sp_err_t sp_sys_read_p(sp_sys_fd_t fd, void* buf, u64 count, u64* bytes_read); @@ -1624,12 +1630,13 @@ SP_API sp_err_t sp_sys_pwrite_p(sp_sys_fd_t fd, const void* buf, u64 count, u SP_API sp_err_t sp_sys_transfer_p(sp_sys_fd_t in, u64* in_pos, sp_sys_fd_t out, u64 count, u64* bytes_moved); SP_API sp_err_t sp_sys_transfer_positional_p(sp_sys_fd_t in, u64* in_pos, sp_sys_fd_t out, u64 count, u64 offset, u64* bytes_moved); SP_API sp_sys_fd_t sp_sys_get_root_p(s32 it); -SP_API s64 sp_sys_get_exe_path_p(c8* buf, u64 size); -SP_API s64 sp_sys_get_cwd_path_p(c8* buf, u64 size); -SP_API s64 sp_sys_get_storage_path_p(c8* buf, u64 size); -SP_API s64 sp_sys_get_config_path_p(c8* buf, u64 size); +SP_API sp_err_t sp_sys_get_root_label_p(s32 it, c8* buf, u64 size, u64* len); +SP_API sp_err_t sp_sys_get_exe_path_p(c8* buf, u64 size, u64* len); +SP_API sp_err_t sp_sys_get_storage_path_p(c8* buf, u64 size, u64* len); +SP_API sp_err_t sp_sys_get_config_path_p(c8* buf, u64 size, u64* len); +SP_API sp_err_t sp_sys_get_fd_path_p(sp_sys_fd_t fd, c8* buf, u64 size, u64* len); SP_API sp_err_t sp_sys_open_p(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_open_mode_t mode, u32 flags, sp_sys_fd_t* out); -SP_API sp_err_t sp_sys_open_dir_p(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_fd_t* out); +SP_API sp_err_t sp_sys_open_dir_p(sp_sys_fd_t fd, const c8* path, u32 len, u32 flags, sp_sys_fd_t* out); SP_API sp_err_t sp_sys_close_p(sp_sys_fd_t fd); SP_API sp_err_t sp_sys_pipe_p(sp_sys_pipe_t* pipe, sp_sys_pipe_desc_t desc); SP_API sp_err_t sp_sys_pipe_ready_p(sp_sys_fd_t fd, u8* ready); @@ -1638,7 +1645,7 @@ SP_API sp_err_t sp_sys_rmdir_p(sp_sys_fd_t fd, const c8* path, u32 len); SP_API sp_err_t sp_sys_unlink_p(sp_sys_fd_t fd, const c8* path, u32 len); SP_API sp_err_t sp_sys_rename_p(sp_sys_fd_t from_fd, const c8* from, u32 from_len, sp_sys_fd_t to_fd, const c8* to, u32 to_len); SP_API sp_err_t sp_sys_link_p(sp_sys_fd_t from_fd, const c8* existing, u32 existing_len, sp_sys_fd_t to_fd, const c8* alias, u32 alias_len); -SP_API sp_err_t sp_sys_symlink_p(const c8* existing, u32 existing_len, sp_sys_fd_t to_fd, const c8* alias, u32 alias_len); +SP_API sp_err_t sp_sys_symlink_p(const c8* existing, u32 existing_len, sp_sys_fd_t to_fd, const c8* alias, u32 alias_len, sp_fs_kind_t kind); SP_API sp_err_t sp_sys_readlink_p(sp_sys_fd_t fd, const c8* path, u32 len, c8* buf, u64 size, u64* target_len); SP_API sp_err_t sp_sys_get_path_metadata_p(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_meta_t* st); SP_API sp_err_t sp_sys_get_link_metadata_p(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_meta_t* st); @@ -1650,7 +1657,6 @@ SP_API sp_err_t sp_sys_nanosleep_p(const sp_sys_timespec_t* req, sp_sys_times SP_API bool sp_sys_futex_wait_p(u32* addr, u32 expected, const sp_sys_timespec_t* timeout); SP_API void sp_sys_futex_wake_p(u32* addr); SP_API void sp_sys_futex_wake_all_p(u32* addr); -SP_API s64 sp_sys_canonicalize_path_p(const c8* path, u32 len, c8* buf, u64 size); SP_API sp_err_t sp_sys_event_open_p(sp_sys_event_t* out); SP_API sp_err_t sp_sys_event_signal_p(sp_sys_event_t event); SP_API sp_err_t sp_sys_event_clear_p(sp_sys_event_t event); @@ -1684,12 +1690,12 @@ SP_API s32 sp_sys_memcmp_p(const void* a, const void* b, u64 n); SP_API void sp_sys_assert_p(bool cond); SP_API void sp_sys_exit_p(s32 code); SP_API void sp_sys_env_p(const c8** env, u32* len); -SP_API s64 sp_sys_lseek_p(sp_sys_fd_t fd, s64 offset, s32 whence); +SP_API sp_err_t sp_sys_lseek_p(sp_sys_fd_t fd, s64 offset, s32 whence, s64* position); SP_API sp_err_t sp_sys_chdir_p(const c8* path, u32 len); -SP_API sp_err_t sp_sys_dir_from_fd_p(sp_sys_fd_t fd, sp_sys_dir_t* out); -SP_API sp_err_t sp_sys_dir_read_p(sp_sys_dir_t* dir, sp_mem_buffer_t* buf); -SP_API sp_err_t sp_sys_dir_parse_p(sp_sys_dir_t* dir, sp_mem_buffer_t* buf, u64* cursor, sp_sys_dir_entry_t* out); -SP_API sp_err_t sp_sys_dir_close_p(sp_sys_dir_t* dir); +SP_API sp_err_t sp_sys_dir_it_open_p(sp_sys_fd_t fd, sp_sys_dir_it_t* out); +SP_API sp_err_t sp_sys_dir_it_read_p(sp_sys_dir_it_t* it, sp_mem_buffer_t* buf); +SP_API sp_err_t sp_sys_dir_it_parse_p(sp_sys_dir_it_t* it, sp_mem_buffer_t* buf, u64* cursor, sp_sys_dir_entry_t* out); +SP_API sp_err_t sp_sys_dir_it_close_p(sp_sys_dir_it_t* it); SP_API const sp_sys_vtable_t sp_sys_vtable_platform; SP_API const sp_sys_vtable_t* sp_sys_set_vtable(const sp_sys_vtable_t* vt); @@ -3017,11 +3023,7 @@ SP_API void sp_tm_epoch_to_iso8601_w(sp_io_writer_t* io, sp_tm_epoc // @fs @filesystem #define SP_PATH_MAX 4096 #define SP_FS_IT_BUF_SIZE SP_SYS_DIR_MIN_BUF - -typedef enum { - SP_FS_LINK_HARD, - SP_FS_LINK_SYMBOLIC, -} sp_fs_link_kind_t; +#define SP_FS_IT_MAX_DEPTH (SP_PATH_MAX / 2 + 1) typedef enum { SP_FS_ATOMIC_REPLACE, @@ -3038,8 +3040,14 @@ typedef enum { SP_FS_PATH_WINDOWS, } sp_fs_path_kind_t; +typedef struct { + sp_sys_fd_t dir; + sp_str_t sub; +} sp_path_t; + typedef struct { sp_str_t path; + sp_str_t rel; sp_str_t name; sp_fs_kind_t kind; } sp_fs_entry_t; @@ -3049,93 +3057,142 @@ typedef struct { sp_fs_kind_t kind; } sp_fs_dir_entry_t; -typedef struct { - sp_sys_dir_t dir; - sp_mem_buffer_t buf; - u64 cursor; -} sp_fs_dir_t; - -typedef struct { - sp_fs_dir_t dir; - SP_ALIGNED u8 buf [SP_FS_IT_BUF_SIZE]; - u32 len; -} sp_fs_it_frame_t; - -typedef struct { - sp_mem_t mem; - sp_fs_entry_t entry; - sp_da(sp_fs_it_frame_t) stack; - c8 path [SP_PATH_MAX]; - bool recursive; - sp_err_t err; -} sp_fs_it_t; - -#define sp_fs_for(mem, dir, it) \ - for (sp_fs_it_t it = sp_fs_it_new(mem, dir); sp_fs_it_next(&it);) -#define sp_fs_for_recursive(mem, dir, it) \ - for (sp_fs_it_t it = sp_fs_it_new_recursive(mem, dir); sp_fs_it_next(&it);) - -SP_API sp_str_t sp_fs_get_name(sp_str_t path); -SP_API sp_str_t sp_fs_parent_path(sp_str_t path); -SP_API sp_str_t sp_fs_trim_path(sp_str_t path); -SP_API sp_str_t sp_fs_normalize_path(sp_mem_t mem, sp_str_t path); -SP_API sp_str_t sp_fs_get_ext(sp_str_t path); -SP_API sp_str_t sp_fs_get_stem(sp_str_t path); SP_API bool sp_fs_is_sep(c8 c); +SP_API bool sp_fs_is_sep_for(c8 c, sp_fs_path_kind_t kind); SP_API bool sp_fs_is_root(sp_str_t path); +SP_API bool sp_fs_is_root_for(sp_str_t path, sp_fs_path_kind_t kind); SP_API bool sp_fs_is_absolute(sp_str_t path); SP_API bool sp_fs_is_absolute_for(sp_str_t path, sp_fs_path_kind_t kind); SP_API bool sp_fs_is_absolute_w(sp_wide_str_t path); -SP_API bool sp_fs_is_glob(sp_str_t path); -SP_API bool sp_fs_glob_match(sp_str_t name, sp_str_t glob); -SP_API sp_str_t sp_fs_join_path(sp_mem_t mem, sp_str_t a, sp_str_t b); +SP_API sp_str_t sp_fs_trim_path(sp_str_t path); +SP_API sp_str_t sp_fs_trim_path_for(sp_str_t path, sp_fs_path_kind_t kind); +SP_API sp_str_t sp_fs_get_name(sp_str_t path); +SP_API sp_str_t sp_fs_get_name_for(sp_str_t path, sp_fs_path_kind_t kind); +SP_API sp_str_t sp_fs_parent_path(sp_str_t path); +SP_API sp_str_t sp_fs_parent_path_for(sp_str_t path, sp_fs_path_kind_t kind); +SP_API sp_str_t sp_fs_get_ext(sp_str_t path); +SP_API sp_str_t sp_fs_get_ext_for(sp_str_t path, sp_fs_path_kind_t kind); +SP_API sp_str_t sp_fs_get_stem(sp_str_t path); +SP_API sp_str_t sp_fs_get_stem_for(sp_str_t path, sp_fs_path_kind_t kind); SP_API sp_str_t sp_fs_replace_ext(sp_mem_t mem, sp_str_t path, sp_str_t ext); +SP_API sp_str_t sp_fs_replace_ext_for(sp_mem_t mem, sp_str_t path, sp_str_t ext, sp_fs_path_kind_t kind); +SP_API sp_str_t sp_fs_join_path(sp_mem_t mem, sp_str_t a, sp_str_t b); +SP_API sp_str_t sp_fs_join_path_for(sp_mem_t mem, sp_str_t a, sp_str_t b, sp_fs_path_kind_t kind); +SP_API sp_str_t sp_fs_normalize_path(sp_mem_t mem, sp_str_t path); +SP_API sp_str_t sp_fs_normalize_path_for(sp_mem_t mem, sp_str_t path, sp_fs_path_kind_t kind); SP_API sp_err_t sp_fs_collect(sp_mem_t mem, sp_str_t path, sp_da(sp_fs_entry_t)* out); SP_API sp_err_t sp_fs_collect_recursive(sp_mem_t mem, sp_str_t path, sp_da(sp_fs_entry_t)* out); SP_API bool sp_fs_exists(sp_str_t path); +SP_API bool sp_fs_exists_at(sp_path_t path); SP_API bool sp_fs_is_file(sp_str_t path); +SP_API bool sp_fs_is_file_at(sp_path_t path); SP_API bool sp_fs_is_dir(sp_str_t path); +SP_API bool sp_fs_is_dir_at(sp_path_t path); SP_API bool sp_fs_is_symlink(sp_str_t path); +SP_API bool sp_fs_is_symlink_at(sp_path_t path); SP_API bool sp_fs_is_target_file(sp_str_t path); +SP_API bool sp_fs_is_target_file_at(sp_path_t path); SP_API bool sp_fs_is_target_dir(sp_str_t path); +SP_API bool sp_fs_is_target_dir_at(sp_path_t path); SP_API sp_fs_kind_t sp_fs_get_kind(sp_str_t path); +SP_API sp_fs_kind_t sp_fs_get_kind_at(sp_path_t path); SP_API sp_fs_kind_t sp_fs_get_target_kind(sp_str_t path); +SP_API sp_fs_kind_t sp_fs_get_target_kind_at(sp_path_t path); SP_API sp_tm_epoch_t sp_fs_get_mod_time(sp_str_t path); +SP_API sp_tm_epoch_t sp_fs_get_mod_time_at(sp_path_t path); SP_API sp_err_t sp_fs_create_dir(sp_str_t path); +SP_API sp_err_t sp_fs_create_dir_at(sp_path_t path); +SP_API sp_err_t sp_fs_create_parent_at(sp_path_t path); SP_API sp_err_t sp_fs_create_file(sp_str_t path); +SP_API sp_err_t sp_fs_create_file_at(sp_path_t path); SP_API sp_err_t sp_fs_create_file_str(sp_str_t path, sp_str_t str); +SP_API sp_err_t sp_fs_create_file_str_at(sp_path_t path, sp_str_t str); SP_API sp_err_t sp_fs_create_file_slice(sp_str_t path, sp_mem_slice_t slice); +SP_API sp_err_t sp_fs_create_file_slice_at(sp_path_t path, sp_mem_slice_t slice); SP_API sp_err_t sp_fs_create_file_cstr(sp_str_t path, const c8* str); +SP_API sp_err_t sp_fs_create_file_cstr_at(sp_path_t path, const c8* str); SP_API sp_err_t sp_fs_remove_dir(sp_str_t path); +SP_API sp_err_t sp_fs_remove_dir_at(sp_path_t path); SP_API sp_err_t sp_fs_remove_file(sp_str_t path); +SP_API sp_err_t sp_fs_remove_file_at(sp_path_t path); SP_API sp_err_t sp_fs_create_hard_link(sp_str_t target, sp_str_t link_path); -SP_API sp_err_t sp_fs_create_sym_link(sp_str_t target, sp_str_t link_path); -SP_API sp_err_t sp_fs_link(sp_str_t from, sp_str_t to, sp_fs_link_kind_t kind); -SP_API sp_err_t sp_fs_copy(sp_str_t from, sp_str_t to); -SP_API sp_err_t sp_fs_copy_into(sp_str_t from, sp_str_t dir); -SP_API sp_err_t sp_fs_copy_file(sp_str_t from, sp_str_t to, sp_fs_atomic_mode_t mode); -SP_API sp_err_t sp_fs_copy_tree(sp_str_t from, sp_str_t to, sp_fs_atomic_mode_t mode); -SP_API sp_err_t sp_fs_dir_open(sp_fs_dir_t* it, sp_sys_fd_t fd, sp_str_t path, sp_mem_slice_t buf); -SP_API sp_err_t sp_fs_dir_next(sp_fs_dir_t* it, sp_fs_dir_entry_t* out); -SP_API sp_err_t sp_fs_dir_close(sp_fs_dir_t* it); -SP_API sp_fs_it_t sp_fs_it_new(sp_mem_t mem, sp_str_t path); -SP_API sp_fs_it_t sp_fs_it_new_recursive(sp_mem_t mem, sp_str_t path); -SP_API bool sp_fs_it_next(sp_fs_it_t* it); -SP_API void sp_fs_it_deinit(sp_fs_it_t* it); - -// literally sys+normalize, if normalize isnt needed this isn't either? -SP_API sp_str_t sp_fs_canonicalize_path(sp_mem_t mem, sp_str_t path); - -// this goes in sys -SP_API sp_str_t sp_fs_resolve(sp_mem_t mem, sp_sys_fd_t fd); - -// just put these in os -SP_API sp_str_t sp_fs_get_cwd(sp_mem_t mem); - -// these currently forward to sys+normalize; remove from fs AND sys and just put in os -SP_API sp_str_t sp_fs_get_exe_path(sp_mem_t mem); -SP_API sp_str_t sp_fs_get_storage_path(sp_mem_t mem); -SP_API sp_str_t sp_fs_get_config_path(sp_mem_t mem); +SP_API sp_err_t sp_fs_create_hard_link_at(sp_path_t target, sp_path_t link_path); +SP_API sp_err_t sp_fs_create_sym_link(sp_str_t target, sp_str_t link_path, sp_fs_kind_t kind); +SP_API sp_err_t sp_fs_create_sym_link_at(sp_str_t target, sp_path_t link_path, sp_fs_kind_t kind); +SP_API sp_err_t sp_fs_copy_at(sp_path_t from, sp_path_t to, sp_fs_atomic_mode_t mode); +SP_API sp_err_t sp_fs_copy_file_at(sp_path_t from, sp_path_t to, sp_fs_atomic_mode_t mode); +SP_API sp_err_t sp_fs_copy_tree_at(sp_path_t from, sp_path_t to, sp_fs_atomic_mode_t mode); +SP_API sp_err_t sp_fs_open_dir_at(sp_path_t path, sp_sys_fd_t* out); +SP_API sp_sys_fd_t sp_fs_get_cwd(); +SP_API sp_err_t sp_fs_get_cwd_path(sp_mem_t mem, sp_str_t* path); +SP_API sp_err_t sp_fs_get_exe_path(sp_mem_t mem, sp_str_t* path); +SP_API sp_err_t sp_fs_get_storage_path(sp_mem_t mem, sp_str_t* path); +SP_API sp_err_t sp_fs_get_config_path(sp_mem_t mem, sp_str_t* path); +SP_API sp_err_t sp_fs_canonicalize_path(sp_mem_t mem, sp_str_t path, sp_str_t* canonical); +SP_API sp_err_t sp_fs_canonicalize_path_at(sp_mem_t mem, sp_path_t path, sp_str_t* canonical); +SP_API sp_path_t sp_path(sp_sys_fd_t dir, sp_str_t sub); +SP_API sp_path_t sp_path_cwd(sp_str_t sub); +SP_API sp_path_t sp_path_from_str(sp_str_t path); +SP_API sp_path_t sp_path_join(sp_mem_t mem, sp_path_t path, sp_str_t sub); +SP_API sp_path_t sp_path_parent(sp_mem_t mem, sp_path_t path); + +/* + sp_fs_it_t + A filesystem iterator + + sp_sys_dir_it_t is the OS level iteration state (usually, just the handle to + the top level directory, but on WASM also a cookie and on macOS a DIR*). The + syscalls are strictly about answering: + - Asking the OS fill a buffer with the next N entries for the handle + - Parsing those opaque bytes into entries + + That can be driven however you want! There's no concept of the "next" entry, + traversing into child directories, or buffer ownership. Syscalls. + + + */ +typedef struct { + sp_sys_dir_it_t dir; + sp_mem_buffer_t buf; + u64 cursor; +} sp_fs_dir_it_t; + +typedef struct { + sp_fs_dir_it_t dir; + SP_ALIGNED u8 buf [SP_FS_IT_BUF_SIZE]; + u32 len; + u32 name_len; +} sp_fs_it_frame_t; + +typedef enum { + SP_FS_IT_ENTRY, + SP_FS_IT_LEAVE, +} sp_fs_it_yield_t; + +typedef struct { + sp_fs_entry_t entry; + sp_path_t at; + sp_fs_it_yield_t yield; + sp_da(sp_fs_it_frame_t) stack; + c8 path [SP_PATH_MAX]; + sp_err_t err; +} sp_fs_it_t; + +#define sp_fs_for(mem, path, it) \ + for (sp_fs_it_t it = sp_fs_it_new_at(mem, path, 0); sp_fs_it_next(&it);) +#define sp_fs_for_recursive(mem, path, it) \ + for (sp_fs_it_t it = sp_fs_it_new_at(mem, path, 0); sp_fs_it_walk(&it);) + +SP_API sp_err_t sp_fs_dir_it_open(sp_fs_dir_it_t* it, sp_path_t path, u32 flags, sp_mem_slice_t buf); +SP_API sp_err_t sp_fs_dir_it_next(sp_fs_dir_it_t* it, sp_fs_dir_entry_t* out); +SP_API sp_err_t sp_fs_dir_it_close(sp_fs_dir_it_t* it); +SP_API sp_fs_it_t sp_fs_it_new(sp_mem_t mem, sp_str_t path); +SP_API sp_fs_it_t sp_fs_it_new_at(sp_mem_t mem, sp_path_t path, u32 flags); +SP_API bool sp_fs_it_next(sp_fs_it_t* it); +SP_API bool sp_fs_it_walk(sp_fs_it_t* it); +SP_API sp_err_t sp_fs_it_enter(sp_fs_it_t* it); +SP_API void sp_fs_it_deinit(sp_fs_it_t* it); + // ███████████ █████ █████ ███████████ ██████████ █████████ ██████████ █████ ██████ █████ █████████ // ░█░░░███░░░█░░███ ░░███ ░░███░░░░░███ ░░███░░░░░█ ███░░░░░███ ░░███░░░░███ ░░███ ░░██████ ░░███ ███░░░░░███ @@ -4634,6 +4691,7 @@ SP_API sp_err_t sp_io_copy_b(sp_io_writer_t* dst, sp_io_reader_t* src, u8* SP_API sp_err_t sp_io_read(sp_io_reader_t* reader, void* ptr, u64 size, u64* bytes_read); SP_API sp_err_t sp_io_read_all(sp_io_reader_t* reader, void* ptr, u64 size, u64* bytes_read); SP_API sp_err_t sp_io_read_file(sp_mem_t mem, sp_str_t path, sp_str_t* content); +SP_API sp_err_t sp_io_read_file_at(sp_mem_t mem, sp_path_t path, sp_str_t* content); SP_API void sp_io_reader_from_mem(sp_io_reader_t* reader, const void* ptr, u64 size); SP_API void sp_io_reader_set_buffer(sp_io_reader_t* reader, u8* buf, u64 capacity); @@ -4663,6 +4721,7 @@ SP_API void sp_io_seeking_reader_from_mem(sp_io_seeking_reader_t* sr, SP_API void sp_io_seeking_reader_from_file_reader(sp_io_seeking_reader_t* sr, sp_io_file_reader_t* fr); SP_API sp_err_t sp_io_file_reader_from_path(sp_io_file_reader_t* r, sp_str_t path); +SP_API sp_err_t sp_io_file_reader_from_path_at(sp_io_file_reader_t* r, sp_path_t path); SP_API sp_err_t sp_io_file_reader_from_file(sp_io_file_reader_t* r, sp_sys_fd_t file, sp_io_close_mode_t mode); SP_API sp_err_t sp_io_file_reader_seek(sp_io_file_reader_t* r, s64 offset, sp_io_whence_t whence, s64* position); SP_API sp_err_t sp_io_file_reader_size(sp_io_file_reader_t* r, u64* size); @@ -4670,6 +4729,7 @@ SP_API sp_err_t sp_io_file_reader_size_force(sp_io_file_reader_t* r, u64* SP_API sp_err_t sp_io_file_reader_close(sp_io_file_reader_t* r); SP_API sp_err_t sp_io_file_writer_from_path(sp_io_file_writer_t* w, sp_str_t path); +SP_API sp_err_t sp_io_file_writer_from_path_at(sp_io_file_writer_t* w, sp_path_t path); SP_API sp_err_t sp_io_file_writer_from_fd(sp_io_file_writer_t* w, sp_sys_fd_t fd, sp_io_close_mode_t close_mode); SP_API sp_err_t sp_io_file_writer_seek(sp_io_file_writer_t* w, s64 offset, sp_io_whence_t whence, s64* position); SP_API sp_err_t sp_io_file_writer_size(sp_io_file_writer_t* w, u64* size); @@ -4709,15 +4769,20 @@ typedef struct { } sp_fs_atomic_t; SP_API sp_err_t sp_fs_atomic_open(sp_fs_atomic_t* af, sp_str_t path); -SP_API sp_err_t sp_fs_atomic_open_at(sp_fs_atomic_t* af, sp_sys_fd_t dir, sp_str_t path); +SP_API sp_err_t sp_fs_atomic_open_at(sp_fs_atomic_t* af, sp_path_t path); SP_API sp_err_t sp_fs_atomic_open_staged(sp_fs_atomic_t* af, sp_str_t path, sp_str_t staging); +SP_API sp_err_t sp_fs_atomic_open_staged_at(sp_fs_atomic_t* af, sp_path_t path, sp_str_t staging); SP_API sp_io_writer_t* sp_fs_atomic_writer(sp_fs_atomic_t* af); SP_API sp_err_t sp_fs_atomic_commit(sp_fs_atomic_t* af, sp_fs_atomic_mode_t mode); SP_API sp_err_t sp_fs_atomic_abort(sp_fs_atomic_t* af); SP_API sp_err_t sp_fs_write_atomic(sp_str_t path, sp_str_t str); +SP_API sp_err_t sp_fs_write_atomic_at(sp_path_t path, sp_str_t str); SP_API sp_err_t sp_fs_write_atomic_slice(sp_str_t path, sp_mem_slice_t slice); +SP_API sp_err_t sp_fs_write_atomic_slice_at(sp_path_t path, sp_mem_slice_t slice); SP_API sp_err_t sp_fs_write_atomic_slice_staged(sp_str_t path, sp_str_t staging, sp_mem_slice_t slice); +SP_API sp_err_t sp_fs_write_atomic_slice_staged_at(sp_path_t path, sp_str_t staging, sp_mem_slice_t slice); SP_API sp_err_t sp_fs_write_atomic_cstr(sp_str_t path, const c8* str); +SP_API sp_err_t sp_fs_write_atomic_cstr_at(sp_path_t path, const c8* str); // ███████████ ███████████ ███████ █████████ ██████████ █████████ █████████ @@ -5056,6 +5121,7 @@ SP_IMP BOOL CALLBACK sp_tls_once_trampoline(PINIT_ONCE once, PVOID param, PVOI // @fs SP_IMP sp_fs_kind_t sp_fs_get_link_kind(sp_str_t path); +SP_IMP sp_fs_kind_t sp_fs_get_link_kind_at(sp_path_t path); // @tls SP_PRIVATE void sp_tls_new(sp_tls_key_t* key, sp_tls_deinit_fn_t fn); @@ -5326,8 +5392,10 @@ SP_IMP DWORD WINAPI sp_win32_thread_launch(LPVOID args); #define SP_SYS_LINUX_O_PATH 010000000 #if defined(SP_AMD64) #define SP_SYS_LINUX_O_DIRECTORY 0200000 + #define SP_SYS_LINUX_O_NOFOLLOW 0400000 #elif defined(SP_ARM64) #define SP_SYS_LINUX_O_DIRECTORY 040000 + #define SP_SYS_LINUX_O_NOFOLLOW 0100000 #endif #define SP_SYS_LINUX_STATX_BASIC_STATS 0x7ff @@ -5661,10 +5729,11 @@ const sp_sys_vtable_t sp_sys_vtable_platform = { .transfer = sp_sys_transfer_p, .transfer_positional = sp_sys_transfer_positional_p, .get_root = sp_sys_get_root_p, + .get_root_label = sp_sys_get_root_label_p, .get_exe_path = sp_sys_get_exe_path_p, - .get_cwd_path = sp_sys_get_cwd_path_p, .get_storage_path = sp_sys_get_storage_path_p, .get_config_path = sp_sys_get_config_path_p, + .get_fd_path = sp_sys_get_fd_path_p, .open = sp_sys_open_p, .open_dir = sp_sys_open_dir_p, .close = sp_sys_close_p, @@ -5687,7 +5756,6 @@ const sp_sys_vtable_t sp_sys_vtable_platform = { .futex_wait = sp_sys_futex_wait_p, .futex_wake = sp_sys_futex_wake_p, .futex_wake_all = sp_sys_futex_wake_all_p, - .canonicalize_path = sp_sys_canonicalize_path_p, .event_open = sp_sys_event_open_p, .event_signal = sp_sys_event_signal_p, .event_clear = sp_sys_event_clear_p, @@ -5723,10 +5791,10 @@ const sp_sys_vtable_t sp_sys_vtable_platform = { .env = sp_sys_env_p, .lseek = sp_sys_lseek_p, .chdir = sp_sys_chdir_p, - .dir_from_fd = sp_sys_dir_from_fd_p, - .dir_read = sp_sys_dir_read_p, - .dir_parse = sp_sys_dir_parse_p, - .dir_close = sp_sys_dir_close_p, + .dir_it_open = sp_sys_dir_it_open_p, + .dir_it_read = sp_sys_dir_it_read_p, + .dir_it_parse = sp_sys_dir_it_parse_p, + .dir_it_close = sp_sys_dir_it_close_p, }; sp_rt_t sp_rt = { @@ -5835,28 +5903,32 @@ sp_sys_fd_t sp_sys_get_root(s32 it) { return (sp_rt.vt->get_root)(it); } -s64 sp_sys_get_exe_path(c8* buf, u64 size) { - return (sp_rt.vt->get_exe_path)(buf, size); +sp_err_t sp_sys_get_root_label(s32 it, c8* buf, u64 size, u64* len) { + return (sp_rt.vt->get_root_label)(it, buf, size, len); +} + +sp_err_t sp_sys_get_exe_path(c8* buf, u64 size, u64* len) { + return (sp_rt.vt->get_exe_path)(buf, size, len); } -s64 sp_sys_get_cwd_path(c8* buf, u64 size) { - return (sp_rt.vt->get_cwd_path)(buf, size); +sp_err_t sp_sys_get_storage_path(c8* buf, u64 size, u64* len) { + return (sp_rt.vt->get_storage_path)(buf, size, len); } -s64 sp_sys_get_storage_path(c8* buf, u64 size) { - return (sp_rt.vt->get_storage_path)(buf, size); +sp_err_t sp_sys_get_config_path(c8* buf, u64 size, u64* len) { + return (sp_rt.vt->get_config_path)(buf, size, len); } -s64 sp_sys_get_config_path(c8* buf, u64 size) { - return (sp_rt.vt->get_config_path)(buf, size); +sp_err_t sp_sys_get_fd_path(sp_sys_fd_t fd, c8* buf, u64 size, u64* len) { + return (sp_rt.vt->get_fd_path)(fd, buf, size, len); } sp_err_t sp_sys_open(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_open_mode_t mode, u32 flags, sp_sys_fd_t* out) { return (sp_rt.vt->open)(fd, path, len, mode, flags, out); } -sp_err_t sp_sys_open_dir(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_fd_t* out) { - return (sp_rt.vt->open_dir)(fd, path, len, out); +sp_err_t sp_sys_open_dir(sp_sys_fd_t fd, const c8* path, u32 len, u32 flags, sp_sys_fd_t* out) { + return (sp_rt.vt->open_dir)(fd, path, len, flags, out); } sp_err_t sp_sys_close(sp_sys_fd_t fd) { @@ -5891,8 +5963,8 @@ sp_err_t sp_sys_link(sp_sys_fd_t from, const c8* existing, u32 existing_len, sp_ return (sp_rt.vt->link)(from, existing, existing_len, to, alias, alias_len); } -sp_err_t sp_sys_symlink(const c8* existing, u32 existing_len, sp_sys_fd_t to_fd, const c8* alias, u32 alias_len) { - return (sp_rt.vt->symlink)(existing, existing_len, to_fd, alias, alias_len); +sp_err_t sp_sys_symlink(const c8* existing, u32 existing_len, sp_sys_fd_t to_fd, const c8* alias, u32 alias_len, sp_fs_kind_t kind) { + return (sp_rt.vt->symlink)(existing, existing_len, to_fd, alias, alias_len, kind); } sp_err_t sp_sys_readlink(sp_sys_fd_t fd, const c8* path, u32 len, c8* buf, u64 size, u64* target_len) { @@ -5939,10 +6011,6 @@ void sp_sys_futex_wake_all(u32* addr) { (sp_rt.vt->futex_wake_all)(addr); } -s64 sp_sys_canonicalize_path(const c8* path, u32 len, c8* buf, u64 size) { - return (sp_rt.vt->canonicalize_path)(path, len, buf, size); -} - sp_err_t sp_sys_event_open(sp_sys_event_t* out) { return (sp_rt.vt->event_open)(out); } @@ -6103,28 +6171,28 @@ void sp_sys_env(const c8** env, u32* len) { (sp_rt.vt->env)(env, len); } -s64 sp_sys_lseek(sp_sys_fd_t fd, s64 offset, s32 whence) { - return (sp_rt.vt->lseek)(fd, offset, whence); +sp_err_t sp_sys_lseek(sp_sys_fd_t fd, s64 offset, s32 whence, s64* position) { + return (sp_rt.vt->lseek)(fd, offset, whence, position); } sp_err_t sp_sys_chdir(const c8* path, u32 len) { return (sp_rt.vt->chdir)(path, len); } -sp_err_t sp_sys_dir_from_fd(sp_sys_fd_t fd, sp_sys_dir_t* out) { - return (sp_rt.vt->dir_from_fd)(fd, out); +sp_err_t sp_sys_dir_it_open(sp_sys_fd_t fd, sp_sys_dir_it_t* out) { + return (sp_rt.vt->dir_it_open)(fd, out); } -sp_err_t sp_sys_dir_read(sp_sys_dir_t* dir, sp_mem_buffer_t* buf) { - return (sp_rt.vt->dir_read)(dir, buf); +sp_err_t sp_sys_dir_it_read(sp_sys_dir_it_t* it, sp_mem_buffer_t* buf) { + return (sp_rt.vt->dir_it_read)(it, buf); } -sp_err_t sp_sys_dir_parse(sp_sys_dir_t* dir, sp_mem_buffer_t* buf, u64* cursor, sp_sys_dir_entry_t* out) { - return (sp_rt.vt->dir_parse)(dir, buf, cursor, out); +sp_err_t sp_sys_dir_it_parse(sp_sys_dir_it_t* it, sp_mem_buffer_t* buf, u64* cursor, sp_sys_dir_entry_t* out) { + return (sp_rt.vt->dir_it_parse)(it, buf, cursor, out); } -sp_err_t sp_sys_dir_close(sp_sys_dir_t* dir) { - return (sp_rt.vt->dir_close)(dir); +sp_err_t sp_sys_dir_it_close(sp_sys_dir_it_t* it) { + return (sp_rt.vt->dir_it_close)(it); } const sp_sys_vtable_t* sp_sys_set_vtable(const sp_sys_vtable_t* vt) { @@ -6172,6 +6240,15 @@ c8** environ; #define SP_S_ISBLK(m) (((m) & SP_S_IFMT) == SP_S_IFBLK) #define SP_S_ISSOCK(m) (((m) & SP_S_IFMT) == SP_S_IFSOCK) +SP_PRIVATE u32 sp_sys_trim_path(const c8* path, u32 len) { + return sp_fs_trim_path(sp_str(path, len)).len; +} + +#if defined(SP_WASM) + #define sp_wasi_import(name) \ + __attribute__((import_module("wasi_snapshot_preview1"), import_name(name))) +#endif + #if defined(SP_WASM_WASI) #include #elif defined(SP_WASM_FREESTANDING) @@ -6189,6 +6266,16 @@ c8** environ; typedef uint16_t __wasi_subclockflags_t; typedef uint8_t __wasi_whence_t; typedef uint32_t __wasi_exitcode_t; + typedef uint64_t __wasi_rights_t; + typedef uint16_t __wasi_oflags_t; + typedef uint32_t __wasi_lookupflags_t; + typedef uint16_t __wasi_fdflags_t; + typedef uint64_t __wasi_dircookie_t; + typedef uint32_t __wasi_dirnamlen_t; + typedef uint64_t __wasi_inode_t; + typedef uint64_t __wasi_device_t; + typedef uint64_t __wasi_linkcount_t; + typedef uint8_t __wasi_filetype_t; typedef struct __wasi_iovec_t { uint8_t* buf; @@ -6239,15 +6326,95 @@ c8** environ; __wasi_subscription_u_t u; } __wasi_subscription_t; - #define __WASI_EVENTTYPE_CLOCK ((__wasi_eventtype_t)0) - #define __WASI_EVENTTYPE_FD_READ ((__wasi_eventtype_t)1) - #define __WASI_EVENTTYPE_FD_WRITE ((__wasi_eventtype_t)2) - #define __WASI_WHENCE_SET ((__wasi_whence_t)0) - #define __WASI_WHENCE_CUR ((__wasi_whence_t)1) - #define __WASI_WHENCE_END ((__wasi_whence_t)2) - - #define sp_wasi_import(name) \ - __attribute__((import_module("wasi_snapshot_preview1"), import_name(name))) + typedef struct __wasi_dirent_t { + __wasi_dircookie_t d_next; + __wasi_inode_t d_ino; + __wasi_dirnamlen_t d_namlen; + __wasi_filetype_t d_type; + } __wasi_dirent_t; + + typedef struct __wasi_filestat_t { + __wasi_device_t dev; + __wasi_inode_t ino; + __wasi_filetype_t filetype; + __wasi_linkcount_t nlink; + __wasi_filesize_t size; + __wasi_timestamp_t atim; + __wasi_timestamp_t mtim; + __wasi_timestamp_t ctim; + } __wasi_filestat_t; + + typedef uint8_t __wasi_preopentype_t; + + typedef struct __wasi_prestat_dir_t { + __wasi_size_t pr_name_len; + } __wasi_prestat_dir_t; + + typedef union __wasi_prestat_u_t { + __wasi_prestat_dir_t dir; + } __wasi_prestat_u_t; + + typedef struct __wasi_prestat_t { + __wasi_preopentype_t tag; + __wasi_prestat_u_t u; + } __wasi_prestat_t; + + #define __WASI_EVENTTYPE_CLOCK ((__wasi_eventtype_t)0) + #define __WASI_EVENTTYPE_FD_READ ((__wasi_eventtype_t)1) + #define __WASI_EVENTTYPE_FD_WRITE ((__wasi_eventtype_t)2) + #define __WASI_WHENCE_SET ((__wasi_whence_t)0) + #define __WASI_WHENCE_CUR ((__wasi_whence_t)1) + #define __WASI_WHENCE_END ((__wasi_whence_t)2) + #define __WASI_DIRCOOKIE_START ((__wasi_dircookie_t)0) + #define __WASI_PREOPENTYPE_DIR ((__wasi_preopentype_t)0) + #define __WASI_FILETYPE_UNKNOWN ((__wasi_filetype_t)0) + #define __WASI_FILETYPE_BLOCK_DEVICE ((__wasi_filetype_t)1) + #define __WASI_FILETYPE_CHARACTER_DEVICE ((__wasi_filetype_t)2) + #define __WASI_FILETYPE_DIRECTORY ((__wasi_filetype_t)3) + #define __WASI_FILETYPE_REGULAR_FILE ((__wasi_filetype_t)4) + #define __WASI_FILETYPE_SOCKET_DGRAM ((__wasi_filetype_t)5) + #define __WASI_FILETYPE_SOCKET_STREAM ((__wasi_filetype_t)6) + #define __WASI_FILETYPE_SYMBOLIC_LINK ((__wasi_filetype_t)7) + #define __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW ((__wasi_lookupflags_t)(1 << 0)) + #define __WASI_OFLAGS_CREAT ((__wasi_oflags_t)(1 << 0)) + #define __WASI_OFLAGS_DIRECTORY ((__wasi_oflags_t)(1 << 1)) + #define __WASI_OFLAGS_EXCL ((__wasi_oflags_t)(1 << 2)) + #define __WASI_OFLAGS_TRUNC ((__wasi_oflags_t)(1 << 3)) + #define __WASI_FDFLAGS_APPEND ((__wasi_fdflags_t)(1 << 0)) + #define __WASI_FDFLAGS_DSYNC ((__wasi_fdflags_t)(1 << 1)) + #define __WASI_FDFLAGS_NONBLOCK ((__wasi_fdflags_t)(1 << 2)) + #define __WASI_FDFLAGS_RSYNC ((__wasi_fdflags_t)(1 << 3)) + #define __WASI_FDFLAGS_SYNC ((__wasi_fdflags_t)(1 << 4)) + #define __WASI_RIGHTS_FD_DATASYNC ((__wasi_rights_t)(1 << 0)) + #define __WASI_RIGHTS_FD_READ ((__wasi_rights_t)(1 << 1)) + #define __WASI_RIGHTS_FD_SEEK ((__wasi_rights_t)(1 << 2)) + #define __WASI_RIGHTS_FD_FDSTAT_SET_FLAGS ((__wasi_rights_t)(1 << 3)) + #define __WASI_RIGHTS_FD_SYNC ((__wasi_rights_t)(1 << 4)) + #define __WASI_RIGHTS_FD_TELL ((__wasi_rights_t)(1 << 5)) + #define __WASI_RIGHTS_FD_WRITE ((__wasi_rights_t)(1 << 6)) + #define __WASI_RIGHTS_FD_ADVISE ((__wasi_rights_t)(1 << 7)) + #define __WASI_RIGHTS_FD_ALLOCATE ((__wasi_rights_t)(1 << 8)) + #define __WASI_RIGHTS_PATH_CREATE_DIRECTORY ((__wasi_rights_t)(1 << 9)) + #define __WASI_RIGHTS_PATH_CREATE_FILE ((__wasi_rights_t)(1 << 10)) + #define __WASI_RIGHTS_PATH_LINK_SOURCE ((__wasi_rights_t)(1 << 11)) + #define __WASI_RIGHTS_PATH_LINK_TARGET ((__wasi_rights_t)(1 << 12)) + #define __WASI_RIGHTS_PATH_OPEN ((__wasi_rights_t)(1 << 13)) + #define __WASI_RIGHTS_FD_READDIR ((__wasi_rights_t)(1 << 14)) + #define __WASI_RIGHTS_PATH_READLINK ((__wasi_rights_t)(1 << 15)) + #define __WASI_RIGHTS_PATH_RENAME_SOURCE ((__wasi_rights_t)(1 << 16)) + #define __WASI_RIGHTS_PATH_RENAME_TARGET ((__wasi_rights_t)(1 << 17)) + #define __WASI_RIGHTS_PATH_FILESTAT_GET ((__wasi_rights_t)(1 << 18)) + #define __WASI_RIGHTS_PATH_FILESTAT_SET_SIZE ((__wasi_rights_t)(1 << 19)) + #define __WASI_RIGHTS_PATH_FILESTAT_SET_TIMES ((__wasi_rights_t)(1 << 20)) + #define __WASI_RIGHTS_FD_FILESTAT_GET ((__wasi_rights_t)(1 << 21)) + #define __WASI_RIGHTS_FD_FILESTAT_SET_SIZE ((__wasi_rights_t)(1 << 22)) + #define __WASI_RIGHTS_FD_FILESTAT_SET_TIMES ((__wasi_rights_t)(1 << 23)) + #define __WASI_RIGHTS_PATH_SYMLINK ((__wasi_rights_t)(1 << 24)) + #define __WASI_RIGHTS_PATH_REMOVE_DIRECTORY ((__wasi_rights_t)(1 << 25)) + #define __WASI_RIGHTS_PATH_UNLINK_FILE ((__wasi_rights_t)(1 << 26)) + #define __WASI_RIGHTS_POLL_FD_READWRITE ((__wasi_rights_t)(1 << 27)) + #define __WASI_RIGHTS_SOCK_SHUTDOWN ((__wasi_rights_t)(1 << 28)) + #define __WASI_RIGHTS_SOCK_ACCEPT ((__wasi_rights_t)(1 << 29)) sp_wasi_import("fd_read") extern __wasi_errno_t __wasi_fd_read(__wasi_fd_t fd, const __wasi_iovec_t* iovs, __wasi_size_t niov, __wasi_size_t* nread); @@ -6289,6 +6456,47 @@ c8** environ; extern _Noreturn void __wasi_proc_exit(__wasi_exitcode_t code); #endif +#if defined(SP_WASM) + sp_wasi_import("path_open") + extern __wasi_errno_t sp_wasi_path_open(__wasi_fd_t fd, __wasi_lookupflags_t dirflags, const c8* path, __wasi_size_t path_len, __wasi_oflags_t oflags, __wasi_rights_t fs_rights_base, __wasi_rights_t fs_rights_inheriting, __wasi_fdflags_t fdflags, __wasi_fd_t* opened); + + sp_wasi_import("fd_readdir") + extern __wasi_errno_t sp_wasi_fd_readdir(__wasi_fd_t fd, u8* buf, __wasi_size_t buf_len, __wasi_dircookie_t cookie, __wasi_size_t* used); + + sp_wasi_import("path_filestat_get") + extern __wasi_errno_t sp_wasi_path_filestat_get(__wasi_fd_t fd, __wasi_lookupflags_t flags, const c8* path, __wasi_size_t path_len, __wasi_filestat_t* filestat); + + sp_wasi_import("fd_filestat_get") + extern __wasi_errno_t sp_wasi_fd_filestat_get(__wasi_fd_t fd, __wasi_filestat_t* filestat); + + sp_wasi_import("fd_prestat_get") + extern __wasi_errno_t sp_wasi_fd_prestat_get(__wasi_fd_t fd, __wasi_prestat_t* prestat); + + sp_wasi_import("fd_prestat_dir_name") + extern __wasi_errno_t sp_wasi_fd_prestat_dir_name(__wasi_fd_t fd, u8* path, __wasi_size_t path_len); + + sp_wasi_import("path_create_directory") + extern __wasi_errno_t sp_wasi_path_create_directory(__wasi_fd_t fd, const c8* path, __wasi_size_t path_len); + + sp_wasi_import("path_remove_directory") + extern __wasi_errno_t sp_wasi_path_remove_directory(__wasi_fd_t fd, const c8* path, __wasi_size_t path_len); + + sp_wasi_import("path_unlink_file") + extern __wasi_errno_t sp_wasi_path_unlink_file(__wasi_fd_t fd, const c8* path, __wasi_size_t path_len); + + sp_wasi_import("path_rename") + extern __wasi_errno_t sp_wasi_path_rename(__wasi_fd_t fd, const c8* old_path, __wasi_size_t old_path_len, __wasi_fd_t new_fd, const c8* new_path, __wasi_size_t new_path_len); + + sp_wasi_import("path_link") + extern __wasi_errno_t sp_wasi_path_link(__wasi_fd_t old_fd, __wasi_lookupflags_t old_flags, const c8* old_path, __wasi_size_t old_path_len, __wasi_fd_t new_fd, const c8* new_path, __wasi_size_t new_path_len); + + sp_wasi_import("path_symlink") + extern __wasi_errno_t sp_wasi_path_symlink(const c8* old_path, __wasi_size_t old_path_len, __wasi_fd_t fd, const c8* new_path, __wasi_size_t new_path_len); + + sp_wasi_import("path_readlink") + extern __wasi_errno_t sp_wasi_path_readlink(__wasi_fd_t fd, const c8* path, __wasi_size_t path_len, u8* buf, __wasi_size_t buf_len, __wasi_size_t* used); +#endif + #if defined(SP_WASM) #define SP_WASI_ESUCCESS 0 #define SP_WASI_EACCES 2 @@ -6317,8 +6525,10 @@ c8** environ; #define SP_WASI_ENOTDIR 54 #define SP_WASI_ENOTEMPTY 55 #define SP_WASI_ENOTSUP 58 +#define SP_WASI_EOVERFLOW 61 #define SP_WASI_EPERM 63 #define SP_WASI_EPIPE 64 +#define SP_WASI_ERANGE 68 #define SP_WASI_EROFS 69 #define SP_WASI_ESPIPE 70 #define SP_WASI_ETIMEDOUT 73 @@ -6366,6 +6576,23 @@ SP_PRIVATE sp_err_t sp_sys_err_from_wasi(__wasi_errno_t e) { default: return SP_ERR_SYS; } } + +SP_PRIVATE sp_err_t sp_sys_wasi_prestat_name(__wasi_fd_t fd, c8* buf, u64 size, u64* len) { + __wasi_prestat_t prestat; + sp_try(sp_sys_err_from_wasi(sp_wasi_fd_prestat_get(fd, &prestat))); + if (prestat.u.dir.pr_name_len >= size) return SP_ERR_SYS_NAME_TOO_LONG; + sp_try(sp_sys_err_from_wasi(sp_wasi_fd_prestat_dir_name(fd, (u8*)buf, prestat.u.dir.pr_name_len))); + buf[prestat.u.dir.pr_name_len] = 0; + *len = prestat.u.dir.pr_name_len; + return SP_OK; +} + +SP_PRIVATE sp_err_t sp_sys_wasi_path(const c8* path, u32* len) { + *len = sp_sys_trim_path(path, *len); + if (!*len) return SP_ERR_SYS_NOT_FOUND; + if (*len >= SP_PATH_MAX) return SP_ERR_SYS_NAME_TOO_LONG; + return SP_OK; +} #endif @@ -6711,12 +6938,16 @@ SP_PRIVATE sp_err_t sp_sys_err_from_errno(s64 e) { } } -SP_PRIVATE sp_err_t sp_sys_posix_path(const c8* path, u32 len, c8 buf[SP_PATH_MAX]) { +SP_PRIVATE sp_err_t sp_sys_posix_cstr(const c8* str, u32 len, c8 buf[SP_PATH_MAX]) { if (len >= SP_PATH_MAX) return SP_ERR_SYS_NAME_TOO_LONG; - sp_mem_copy(buf, path, len); + sp_mem_copy(buf, str, len); buf[len] = 0; return SP_OK; } + +SP_PRIVATE sp_err_t sp_sys_posix_path(const c8* path, u32 len, c8 buf[SP_PATH_MAX]) { + return sp_sys_posix_cstr(path, sp_sys_trim_path(path, len), buf); +} #endif #if defined(SP_MACOS) || defined(SP_COSMO) @@ -6916,6 +7147,15 @@ SP_PRIVATE sp_wide_str_t sp_sys_dos_from_nt_wtf16(u16* nt, u32 len) { return out; } +SP_PRIVATE s64 sp_sys_win32_copy_path(sp_str_t path, c8* buf, u64 size) { + if (path.len >= size) return -1; + sp_for(it, path.len) { + buf[it] = path.data[it] == '\\' ? '/' : path.data[it]; + } + buf[path.len] = 0; + return (s64)path.len; +} + typedef struct { sp_nt_unicode_string_t name; HANDLE root; @@ -6923,6 +7163,7 @@ typedef struct { SP_PRIVATE sp_nt_status_t sp_sys_nt_target(sp_sys_fd_t root_fd, sp_str_t utf8, u16* result, u32 result_cap, sp_sys_nt_target_t* out) { *out = sp_zero_s(sp_sys_nt_target_t); + utf8 = sp_fs_trim_path(utf8); if (sp_str_empty(utf8)) return SP_NT_STATUS_OBJECT_NAME_NOT_FOUND; if (utf8.len >= SP_PATH_MAX) return SP_NT_STATUS_NAME_TOO_LONG; @@ -7118,7 +7359,6 @@ SP_PRIVATE sp_err_t sp_sys_nt_delete(sp_sys_fd_t fd, sp_str_t path, bool dir) { // We have to conform to the lowest common denominator, which in this case // is POSIX. That means that unlink() is for everything except directories, // and rmdir is only for directories. - path = sp_fs_trim_path(path); u32 options = SP_NT_FILE_SYNCHRONOUS_IO_NONALERT | SP_NT_FILE_OPEN_REPARSE_POINT; if (dir) options |= SP_NT_FILE_DIRECTORY_FILE; @@ -7252,11 +7492,8 @@ static void sp_sys_timespec_from_filetime(FILETIME ft, sp_sys_timespec_t* out) { } static sp_err_t sp_sys_file_meta_from_nt_path(sp_sys_fd_t dir, sp_str_t path, sp_sys_file_meta_t* out, bool follow_symlinks) { - sp_str_t trimmed = sp_fs_trim_path(path); - bool trailing = trimmed.len != path.len; - path = trimmed; u32 options = SP_NT_FILE_SYNCHRONOUS_IO_NONALERT | SP_NT_FILE_OPEN_FOR_BACKUP_INTENT; - if (!follow_symlinks && !trailing) options |= SP_NT_FILE_OPEN_REPARSE_POINT; + if (!follow_symlinks) options |= SP_NT_FILE_OPEN_REPARSE_POINT; sp_sys_fd_t fd = SP_SYS_INVALID_FD; sp_nt_status_t status = sp_sys_nt_open( dir, @@ -7268,7 +7505,6 @@ static sp_err_t sp_sys_file_meta_from_nt_path(sp_sys_fd_t dir, sp_str_t path, sp if (!SP_NT_SUCCESS(status)) return sp_sys_err_from_nt(status); sp_err_t rc = sp_sys_get_file_metadata_p(fd, out); sp_sys_nt_close(fd); - if (!rc && trailing && out->kind != SP_FS_KIND_DIR) return SP_ERR_SYS_NOT_DIR; return rc; } @@ -7291,10 +7527,25 @@ SP_PRIVATE sp_err_t sp_sys_tty_err_from_win32(DWORD err) { SP_PRIVATE size_t sp_sys_posix_io_count(u64 count) { return count > SP_SYS_POSIX_IO_MAX ? SP_SYS_POSIX_IO_MAX : (size_t)count; } +#endif -SP_PRIVATE u32 sp_sys_posix_trim_slashes(const c8* path, u32 len) { - while (len > 1 && path[len - 1] == '/') len--; - return len; +#if defined(SP_WASM) +SP_PRIVATE sp_sys_timespec_t sp_sys_timespec_from_ns(u64 ns); + +static void sp_sys_file_meta_from_wasi(const __wasi_filestat_t* src, sp_sys_file_meta_t* meta) { + if (src->filetype == __WASI_FILETYPE_SYMBOLIC_LINK) meta->kind = SP_FS_KIND_SYMLINK; + else if (src->filetype == __WASI_FILETYPE_DIRECTORY) meta->kind = SP_FS_KIND_DIR; + else if (src->filetype == __WASI_FILETYPE_REGULAR_FILE) meta->kind = SP_FS_KIND_FILE; + else meta->kind = SP_FS_KIND_NONE; + + meta->size = (s64)src->size; + meta->atime = sp_sys_timespec_from_ns(src->atim); + meta->mtime = sp_sys_timespec_from_ns(src->mtim); + meta->btime = sp_zero_s(sp_sys_timespec_t); + meta->id = src->ino; + meta->device = src->dev; + meta->nlink = src->nlink; + meta->perms = meta->kind == SP_FS_KIND_DIR ? sp_sys_default_dir_perms : sp_sys_default_file_perms; } #endif @@ -7373,8 +7624,10 @@ sp_err_t sp_sys_get_file_metadata_p(sp_sys_fd_t fd, sp_sys_file_meta_t* meta) { return SP_OK; #elif defined(SP_WASM) - (void)fd; (void)meta; - return SP_ERR_SYS_UNSUPPORTED; + __wasi_filestat_t native = sp_zero; + sp_try(sp_sys_err_from_wasi(sp_wasi_fd_filestat_get((__wasi_fd_t)fd, &native))); + sp_sys_file_meta_from_wasi(&native, meta); + return SP_OK; #else #error "sp_sys_get_file_metadata" @@ -7416,8 +7669,9 @@ sp_err_t sp_sys_rename_p(sp_sys_fd_t from_fd, const c8* from, u32 from_len, sp_s return sp_sys_err_from_libc(renameat(from_fd, buffers.from, to_fd, buffers.to)); #elif defined(SP_WASM) - (void)from_fd; (void)from; (void)from_len; (void)to_fd; (void)to; (void)to_len; - return SP_ERR_SYS_UNSUPPORTED; + sp_try(sp_sys_wasi_path(from, &from_len)); + sp_try(sp_sys_wasi_path(to, &to_len)); + return sp_sys_err_from_wasi(sp_wasi_path_rename((__wasi_fd_t)from_fd, from, from_len, (__wasi_fd_t)to_fd, to, to_len)); #else #error "sp_sys_rename" @@ -8099,19 +8353,22 @@ sp_err_t sp_sys_transfer_positional_p(sp_sys_fd_t in, u64* in_pos, sp_sys_fd_t o ////////////////// // SP_SYS_LSEEK // ////////////////// -s64 sp_sys_lseek_p(sp_sys_fd_t fd, s64 offset, s32 whence) { +sp_err_t sp_sys_lseek_p(sp_sys_fd_t fd, s64 offset, s32 whence, s64* position) { + if (position) *position = 0; + #if defined(SP_WIN32) DWORD method; switch (whence) { case SP_IO_SEEK_SET: method = FILE_BEGIN; break; case SP_IO_SEEK_CUR: method = FILE_CURRENT; break; case SP_IO_SEEK_END: method = FILE_END; break; - default: return -1; + default: return SP_ERR_SYS_INVALID; } LARGE_INTEGER dist = { .QuadPart = offset }; LARGE_INTEGER new_pos; - if (!SetFilePointerEx((HANDLE)fd, dist, &new_pos, method)) return -1; - return (s64)new_pos.QuadPart; + if (!SetFilePointerEx((HANDLE)fd, dist, &new_pos, method)) return sp_sys_err_from_win32(GetLastError()); + if (position) *position = (s64)new_pos.QuadPart; + return SP_OK; #elif defined(SP_LINUX) s32 native; @@ -8119,10 +8376,12 @@ s64 sp_sys_lseek_p(sp_sys_fd_t fd, s64 offset, s32 whence) { case SP_IO_SEEK_SET: native = 0; break; case SP_IO_SEEK_CUR: native = 1; break; case SP_IO_SEEK_END: native = 2; break; - default: return -1; + default: return SP_ERR_SYS_INVALID; } s64 rc = sp_syscall(SP_SYSCALL_NUM_LSEEK, fd, offset, native); - return rc < 0 ? -1 : rc; + if (rc < 0) return sp_sys_err_from_errno(-rc); + if (position) *position = rc; + return SP_OK; #elif defined(SP_MACOS) || defined(SP_COSMO) s32 native; @@ -8130,9 +8389,12 @@ s64 sp_sys_lseek_p(sp_sys_fd_t fd, s64 offset, s32 whence) { case SP_IO_SEEK_SET: native = SEEK_SET; break; case SP_IO_SEEK_CUR: native = SEEK_CUR; break; case SP_IO_SEEK_END: native = SEEK_END; break; - default: return -1; + default: return SP_ERR_SYS_INVALID; } - return lseek(fd, offset, native); + s64 rc = lseek(fd, offset, native); + if (rc < 0) return sp_sys_err_from_errno(errno); + if (position) *position = rc; + return SP_OK; #elif defined(SP_WASM) __wasi_whence_t native; @@ -8140,11 +8402,12 @@ s64 sp_sys_lseek_p(sp_sys_fd_t fd, s64 offset, s32 whence) { case SP_IO_SEEK_SET: native = __WASI_WHENCE_SET; break; case SP_IO_SEEK_CUR: native = __WASI_WHENCE_CUR; break; case SP_IO_SEEK_END: native = __WASI_WHENCE_END; break; - default: return -1; + default: return SP_ERR_SYS_INVALID; } __wasi_filesize_t newoff = 0; - __wasi_errno_t err = __wasi_fd_seek((__wasi_fd_t)fd, (__wasi_filedelta_t)offset, native, &newoff); - return err ? -1 : (s64)newoff; + sp_try(sp_sys_err_from_wasi(__wasi_fd_seek((__wasi_fd_t)fd, (__wasi_filedelta_t)offset, native, &newoff))); + if (position) *position = (s64)newoff; + return SP_OK; #else #error "sp_sys_lseek" @@ -8222,6 +8485,7 @@ SP_PRIVATE u32 sp_sys_nt_access_from_mode(sp_sys_open_mode_t mode, u32 flags) { case SP_SYS_OPEN_MODE_RO: { access |= read; break; } case SP_SYS_OPEN_MODE_WO: { access |= write; break; } case SP_SYS_OPEN_MODE_RW: { access |= read | write; break; } + case SP_SYS_OPEN_MODE_PATH: { break; } } return access; } @@ -8241,6 +8505,7 @@ SP_PRIVATE u32 sp_sys_linux_open_flags(sp_sys_open_mode_t mode, u32 flags) { case SP_SYS_OPEN_MODE_RO: { o |= SP_SYS_LINUX_O_RDONLY; break; } case SP_SYS_OPEN_MODE_WO: { o |= SP_SYS_LINUX_O_WRONLY; break; } case SP_SYS_OPEN_MODE_RW: { o |= SP_SYS_LINUX_O_RDWR; break; } + case SP_SYS_OPEN_MODE_PATH: { o |= SP_SYS_LINUX_O_PATH; break; } } if (flags & (SP_SYS_OPEN_CREATE | SP_SYS_OPEN_EXCLUSIVE)) o |= SP_SYS_LINUX_O_CREAT; if (flags & SP_SYS_OPEN_EXCLUSIVE) o |= SP_SYS_LINUX_O_EXCL; @@ -8256,6 +8521,7 @@ SP_PRIVATE s32 sp_sys_posix_open_flags(sp_sys_open_mode_t mode, u32 flags) { case SP_SYS_OPEN_MODE_RO: { o |= O_RDONLY; break; } case SP_SYS_OPEN_MODE_WO: { o |= O_WRONLY; break; } case SP_SYS_OPEN_MODE_RW: { o |= O_RDWR; break; } + case SP_SYS_OPEN_MODE_PATH: { o |= O_RDONLY | O_NONBLOCK; break; } } if (flags & (SP_SYS_OPEN_CREATE | SP_SYS_OPEN_EXCLUSIVE)) o |= O_CREAT; if (flags & SP_SYS_OPEN_EXCLUSIVE) o |= O_EXCL; @@ -8264,21 +8530,53 @@ SP_PRIVATE s32 sp_sys_posix_open_flags(sp_sys_open_mode_t mode, u32 flags) { return o; } +#elif defined(SP_WASM) +#define SP_SYS_WASI_DIR_RIGHTS ( \ + __WASI_RIGHTS_FD_READ | __WASI_RIGHTS_FD_READDIR | __WASI_RIGHTS_FD_FILESTAT_GET | __WASI_RIGHTS_FD_FDSTAT_SET_FLAGS | \ + __WASI_RIGHTS_PATH_OPEN | __WASI_RIGHTS_PATH_CREATE_DIRECTORY | __WASI_RIGHTS_PATH_CREATE_FILE | \ + __WASI_RIGHTS_PATH_LINK_SOURCE | __WASI_RIGHTS_PATH_LINK_TARGET | __WASI_RIGHTS_PATH_SYMLINK | __WASI_RIGHTS_PATH_READLINK | \ + __WASI_RIGHTS_PATH_RENAME_SOURCE | __WASI_RIGHTS_PATH_RENAME_TARGET | \ + __WASI_RIGHTS_PATH_FILESTAT_GET | __WASI_RIGHTS_PATH_FILESTAT_SET_SIZE | __WASI_RIGHTS_PATH_FILESTAT_SET_TIMES | \ + __WASI_RIGHTS_PATH_REMOVE_DIRECTORY | __WASI_RIGHTS_PATH_UNLINK_FILE) + +SP_PRIVATE __wasi_rights_t sp_sys_wasi_open_rights(sp_sys_open_mode_t mode) { + __wasi_rights_t common = __WASI_RIGHTS_FD_SEEK | __WASI_RIGHTS_FD_TELL | __WASI_RIGHTS_FD_ADVISE | __WASI_RIGHTS_FD_FILESTAT_GET | __WASI_RIGHTS_FD_FDSTAT_SET_FLAGS | __WASI_RIGHTS_POLL_FD_READWRITE; + __wasi_rights_t read = __WASI_RIGHTS_FD_READ; + __wasi_rights_t write = __WASI_RIGHTS_FD_WRITE | __WASI_RIGHTS_FD_DATASYNC | __WASI_RIGHTS_FD_SYNC | __WASI_RIGHTS_FD_ALLOCATE | __WASI_RIGHTS_FD_FILESTAT_SET_SIZE | __WASI_RIGHTS_FD_FILESTAT_SET_TIMES; + switch (mode) { + case SP_SYS_OPEN_MODE_RO: return common | read; + case SP_SYS_OPEN_MODE_WO: return common | write; + case SP_SYS_OPEN_MODE_RW: return common | read | write; + case SP_SYS_OPEN_MODE_PATH: return common; + } + return common; +} + +SP_PRIVATE __wasi_oflags_t sp_sys_wasi_open_flags(u32 flags) { + __wasi_oflags_t o = 0; + if (flags & (SP_SYS_OPEN_CREATE | SP_SYS_OPEN_EXCLUSIVE)) o |= __WASI_OFLAGS_CREAT; + if (flags & SP_SYS_OPEN_EXCLUSIVE) o |= __WASI_OFLAGS_EXCL; + if (flags & SP_SYS_OPEN_TRUNCATE) o |= __WASI_OFLAGS_TRUNC; + return o; +} + #endif sp_err_t sp_sys_open_p(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_open_mode_t mode, u32 flags, sp_sys_fd_t* out) { *out = SP_SYS_INVALID_FD; if (flags & SP_SYS_OPEN_TRUNCATE) { - if (mode == SP_SYS_OPEN_MODE_RO) return SP_ERR_SYS_INVALID; - if (flags & SP_SYS_OPEN_APPEND) return SP_ERR_SYS_INVALID; + if (mode == SP_SYS_OPEN_MODE_RO) return SP_ERR_SYS_INVALID; + if (mode == SP_SYS_OPEN_MODE_PATH) return SP_ERR_SYS_INVALID; + if (flags & SP_SYS_OPEN_APPEND) return SP_ERR_SYS_INVALID; } #if defined(SP_WIN32) u32 access = sp_sys_nt_access_from_mode(mode, flags); u32 share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; u32 disposition = sp_sys_nt_disposition_from_flags(flags); - u32 options = SP_NT_FILE_SYNCHRONOUS_IO_NONALERT | SP_NT_FILE_OPEN_FOR_BACKUP_INTENT | SP_NT_FILE_NON_DIRECTORY_FILE; + u32 options = SP_NT_FILE_SYNCHRONOUS_IO_NONALERT | SP_NT_FILE_OPEN_FOR_BACKUP_INTENT; + if (mode != SP_SYS_OPEN_MODE_PATH) options |= SP_NT_FILE_NON_DIRECTORY_FILE; if (flags & SP_SYS_OPEN_EXCLUSIVE) options |= SP_NT_FILE_OPEN_REPARSE_POINT; sp_sys_fd_t handle = SP_SYS_INVALID_FD; @@ -8307,25 +8605,54 @@ sp_err_t sp_sys_open_p(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_open_mode return SP_OK; #elif defined(SP_WASM) - (void)fd; (void)path; (void)len; (void)mode; (void)flags; - return SP_ERR_SYS_UNSUPPORTED; + sp_try(sp_sys_wasi_path(path, &len)); + __wasi_rights_t rights = sp_sys_wasi_open_rights(mode); + __wasi_fdflags_t fdflags = (flags & SP_SYS_OPEN_APPEND) ? __WASI_FDFLAGS_APPEND : 0; + __wasi_fd_t opened = -1; + sp_try(sp_sys_err_from_wasi(sp_wasi_path_open((__wasi_fd_t)fd, __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW, path, len, sp_sys_wasi_open_flags(flags), rights, rights, fdflags, &opened))); + + *out = (sp_sys_fd_t)opened; + return SP_OK; #else #error "sp_sys_open" #endif } -sp_err_t sp_sys_open_dir_p(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_fd_t* out) { +SP_PRIVATE sp_err_t sp_sys_open_dir_nofollow_err(sp_sys_fd_t fd, const c8* path, u32 len, u32 flags, sp_err_t err) { + if (err != SP_ERR_SYS_NOT_DIR || !(flags & SP_SYS_OPEN_DIR_NO_FOLLOW)) return err; + + sp_sys_file_meta_t st = sp_zero; + if (sp_sys_get_link_metadata_p(fd, path, len, &st)) return err; + return st.kind == SP_FS_KIND_SYMLINK ? SP_ERR_SYS_LOOP : err; +} + +sp_err_t sp_sys_open_dir_p(sp_sys_fd_t fd, const c8* path, u32 len, u32 flags, sp_sys_fd_t* out) { *out = SP_SYS_INVALID_FD; + if (flags & ~(u32)SP_SYS_OPEN_DIR_NO_FOLLOW) return SP_ERR_SYS_INVALID; #if defined(SP_WIN32) u32 access = SYNCHRONIZE | FILE_READ_ATTRIBUTES | FILE_READ_DATA | FILE_READ_EA; u32 share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; u32 options = SP_NT_FILE_SYNCHRONOUS_IO_NONALERT | SP_NT_FILE_OPEN_FOR_BACKUP_INTENT | SP_NT_FILE_DIRECTORY_FILE; + if (flags & SP_SYS_OPEN_DIR_NO_FOLLOW) options |= SP_NT_FILE_OPEN_REPARSE_POINT; sp_sys_fd_t handle = SP_SYS_INVALID_FD; sp_nt_status_t status = sp_sys_nt_open(fd, sp_str(path, len), access, share, SP_NT_FILE_OPEN, options, FILE_ATTRIBUTE_NORMAL, &handle); - if (!SP_NT_SUCCESS(status)) return sp_sys_err_from_nt(status); + if (!SP_NT_SUCCESS(status)) return sp_sys_open_dir_nofollow_err(fd, path, len, flags, sp_sys_err_from_nt(status)); + + if (flags & SP_SYS_OPEN_DIR_NO_FOLLOW) { + BY_HANDLE_FILE_INFORMATION info = sp_zero; + if (!GetFileInformationByHandle((HANDLE)handle, &info)) { + sp_err_t err = sp_sys_err_from_win32(GetLastError()); + sp_sys_nt_close(handle); + return err; + } + if (info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) { + sp_sys_nt_close(handle); + return SP_ERR_SYS_LOOP; + } + } *out = handle; return SP_OK; @@ -8333,8 +8660,10 @@ sp_err_t sp_sys_open_dir_p(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_fd_t* #elif defined(SP_LINUX) c8 buf [SP_PATH_MAX]; sp_try(sp_sys_posix_path(path, len, buf)); - s64 rc = sp_syscall(SP_SYSCALL_NUM_OPENAT, fd, buf, SP_SYS_LINUX_O_RDONLY | SP_SYS_LINUX_O_DIRECTORY | SP_SYS_LINUX_O_CLOEXEC, 0); - if (rc < 0) return sp_sys_err_from_errno(-rc); + u32 o = SP_SYS_LINUX_O_RDONLY | SP_SYS_LINUX_O_DIRECTORY | SP_SYS_LINUX_O_CLOEXEC; + if (flags & SP_SYS_OPEN_DIR_NO_FOLLOW) o |= SP_SYS_LINUX_O_NOFOLLOW; + s64 rc = sp_syscall(SP_SYSCALL_NUM_OPENAT, fd, buf, o, 0); + if (rc < 0) return sp_sys_open_dir_nofollow_err(fd, path, len, flags, sp_sys_err_from_errno(-rc)); *out = (sp_sys_fd_t)rc; return SP_OK; @@ -8342,15 +8671,24 @@ sp_err_t sp_sys_open_dir_p(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_fd_t* #elif defined(SP_MACOS) || defined(SP_COSMO) c8 buf [SP_PATH_MAX]; sp_try(sp_sys_posix_path(path, len, buf)); - s32 rc = openat((int)fd, buf, O_RDONLY | O_DIRECTORY | O_CLOEXEC, 0); - if (rc < 0) return sp_sys_err_from_errno(errno); + s32 o = O_RDONLY | O_DIRECTORY | O_CLOEXEC; + if (flags & SP_SYS_OPEN_DIR_NO_FOLLOW) o |= O_NOFOLLOW; + s32 rc = openat((int)fd, buf, o, 0); + if (rc < 0) return sp_sys_open_dir_nofollow_err(fd, path, len, flags, sp_sys_err_from_errno(errno)); *out = (sp_sys_fd_t)rc; return SP_OK; #elif defined(SP_WASM) - (void)fd; (void)path; (void)len; - return SP_ERR_SYS_UNSUPPORTED; + sp_try(sp_sys_wasi_path(path, &len)); + __wasi_lookupflags_t lookup = (flags & SP_SYS_OPEN_DIR_NO_FOLLOW) ? 0 : __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW; + __wasi_rights_t inheriting = SP_SYS_WASI_DIR_RIGHTS | sp_sys_wasi_open_rights(SP_SYS_OPEN_MODE_RW); + __wasi_fd_t opened = -1; + __wasi_errno_t err = sp_wasi_path_open((__wasi_fd_t)fd, lookup, path, len, __WASI_OFLAGS_DIRECTORY, SP_SYS_WASI_DIR_RIGHTS, inheriting, 0, &opened); + if (err) return sp_sys_open_dir_nofollow_err(fd, path, len, flags, sp_sys_err_from_wasi(err)); + + *out = (sp_sys_fd_t)opened; + return SP_OK; #else #error "sp_sys_open_dir" @@ -8362,8 +8700,8 @@ sp_err_t sp_sys_open_s(sp_sys_fd_t fd, sp_str_t path, sp_sys_open_mode_t mode, u return sp_sys_open(fd, path.data, path.len, mode, flags, out); } -sp_err_t sp_sys_open_dir_s(sp_sys_fd_t fd, sp_str_t path, sp_sys_fd_t* out) { - return sp_sys_open_dir(fd, path.data, path.len, out); +sp_err_t sp_sys_open_dir_s(sp_sys_fd_t fd, sp_str_t path, u32 flags, sp_sys_fd_t* out) { + return sp_sys_open_dir(fd, path.data, path.len, flags, out); } ///////////////////// @@ -8381,6 +8719,8 @@ sp_sys_fd_t sp_sys_get_root_p(s32 it) { return (sp_sys_fd_t)SP_AT_FDCWD; #elif defined(SP_WASM) + __wasi_prestat_t prestat; + if (sp_wasi_fd_prestat_get((__wasi_fd_t)(3 + it), &prestat)) return SP_SYS_INVALID_FD; return (sp_sys_fd_t)(3 + it); #else @@ -8388,6 +8728,26 @@ sp_sys_fd_t sp_sys_get_root_p(s32 it) { #endif } +sp_err_t sp_sys_get_root_label_p(s32 it, c8* buf, u64 size, u64* len) { + *len = 0; + +#if defined(SP_WIN32) || defined(SP_LINUX) || defined(SP_MACOS) || defined(SP_COSMO) + if (it != 0) return SP_ERR_SYS_BAD_FD; + if (!size) return SP_ERR_SYS_NAME_TOO_LONG; + buf[0] = 0; + return SP_OK; + +#elif defined(SP_WASM) + sp_try(sp_sys_wasi_prestat_name((__wasi_fd_t)(3 + it), buf, size, len)); + *len = sp_sys_trim_path(buf, (u32)*len); + buf[*len] = 0; + return SP_OK; + +#else + #error "sp_sys_get_root_label" +#endif +} + ////////////////////// // SP_SYS_NANOSLEEP // ////////////////////// @@ -8553,48 +8913,58 @@ void sp_sys_futex_wake_all_p(u32* addr) { } #if defined(SP_WIN32) -static s64 sp_sys_copy_env_var(const c8* name, c8* buf, u64 size) { - DWORD len = GetEnvironmentVariableA(name, buf, (DWORD)size); - if (len == 0 || len >= size) return -1; - return (s64)len; +static sp_err_t sp_sys_copy_env_path(const c8* name, c8* buf, u64 size, u64* len) { + *len = 0; + DWORD n = GetEnvironmentVariableA(name, buf, (DWORD)size); + if (n == 0) return SP_ERR_SYS_NOT_FOUND; + if (n >= size) return SP_ERR_SYS_NAME_TOO_LONG; + *len = (u64)sp_sys_win32_copy_path(sp_fs_trim_path(sp_str(buf, n)), buf, size); + return SP_OK; } -s64 sp_sys_get_storage_path_p(c8* buf, u64 size) { - return sp_sys_copy_env_var("LOCALAPPDATA", buf, size); +sp_err_t sp_sys_get_storage_path_p(c8* buf, u64 size, u64* len) { + return sp_sys_copy_env_path("LOCALAPPDATA", buf, size, len); } -s64 sp_sys_get_config_path_p(c8* buf, u64 size) { - return sp_sys_copy_env_var("APPDATA", buf, size); +sp_err_t sp_sys_get_config_path_p(c8* buf, u64 size, u64* len) { + return sp_sys_copy_env_path("APPDATA", buf, size, len); } #else -static s64 sp_sys_xdg_or_home(sp_str_t xdg_var, sp_str_t home_suffix, c8* buf, u64 size) { +static sp_err_t sp_sys_xdg_or_home(sp_str_t xdg_var, sp_str_t home_suffix, c8* buf, u64 size, u64* len) { + *len = 0; sp_mem_arena_marker_t scratch = sp_mem_begin_scratch(); - sp_str_t value = sp_os_env_get(xdg_var); + sp_str_t value = sp_fs_trim_path(sp_os_env_get(xdg_var)); if (sp_str_empty(value)) { sp_str_t home = sp_os_env_get(sp_str_lit("HOME")); if (!sp_str_empty(home)) { - value = sp_str_join(sp_mem_get_scratch(), home, home_suffix, sp_str_lit("/")); + value = sp_fs_join_path(scratch.mem, home, home_suffix); } } - s64 result = -1; - if (!sp_str_empty(value) && value.len < size) { - for (u32 i = 0; i < value.len; i++) buf[i] = value.data[i]; - buf[value.len] = '\0'; - result = (s64)value.len; + sp_err_t err = SP_OK; + if (sp_str_empty(value)) { + err = SP_ERR_SYS_NOT_FOUND; + } + else if (value.len >= size) { + err = SP_ERR_SYS_NAME_TOO_LONG; + } + else { + sp_mem_copy(buf, value.data, value.len); + buf[value.len] = 0; + *len = value.len; } sp_mem_end_scratch(scratch); - return result; + return err; } -s64 sp_sys_get_storage_path_p(c8* buf, u64 size) { - return sp_sys_xdg_or_home(sp_str_lit("XDG_DATA_HOME"), sp_str_lit(".local/share"), buf, size); +sp_err_t sp_sys_get_storage_path_p(c8* buf, u64 size, u64* len) { + return sp_sys_xdg_or_home(sp_str_lit("XDG_DATA_HOME"), sp_str_lit(".local/share"), buf, size, len); } -s64 sp_sys_get_config_path_p(c8* buf, u64 size) { - return sp_sys_xdg_or_home(sp_str_lit("XDG_CONFIG_HOME"), sp_str_lit(".config"), buf, size); +sp_err_t sp_sys_get_config_path_p(c8* buf, u64 size, u64* len) { + return sp_sys_xdg_or_home(sp_str_lit("XDG_CONFIG_HOME"), sp_str_lit(".config"), buf, size, len); } #endif @@ -10160,13 +10530,15 @@ sp_err_t sp_sys_get_path_metadata_p(sp_sys_fd_t fd, const c8* path, u32 len, sp_ sp_try(sp_sys_posix_path(path, len, buf)); struct stat native; sp_try(sp_sys_err_from_libc(fstatat(fd, buf, &native, 0))); - if (path[len - 1] == '/' && !S_ISDIR(native.st_mode)) return SP_ERR_SYS_NOT_DIR; sp_sys_file_meta_from_libc(&native, st); return SP_OK; #elif defined(SP_WASM) - (void)fd; (void)path; (void)len; (void)st; - return SP_ERR_SYS_UNSUPPORTED; + sp_try(sp_sys_wasi_path(path, &len)); + __wasi_filestat_t native = sp_zero; + sp_try(sp_sys_err_from_wasi(sp_wasi_path_filestat_get((__wasi_fd_t)fd, __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW, path, len, &native))); + sp_sys_file_meta_from_wasi(&native, st); + return SP_OK; #else #error "sp_sys_get_path_metadata" @@ -10197,13 +10569,15 @@ sp_err_t sp_sys_get_link_metadata_p(sp_sys_fd_t fd, const c8* path, u32 len, sp_ sp_try(sp_sys_posix_path(path, len, buf)); struct stat native; sp_try(sp_sys_err_from_libc(fstatat(fd, buf, &native, AT_SYMLINK_NOFOLLOW))); - if (path[len - 1] == '/' && !S_ISDIR(native.st_mode)) return SP_ERR_SYS_NOT_DIR; sp_sys_file_meta_from_libc(&native, st); return SP_OK; #elif defined(SP_WASM) - (void)fd; (void)path; (void)len; (void)st; - return SP_ERR_SYS_UNSUPPORTED; + sp_try(sp_sys_wasi_path(path, &len)); + __wasi_filestat_t native = sp_zero; + sp_try(sp_sys_err_from_wasi(sp_wasi_path_filestat_get((__wasi_fd_t)fd, 0, path, len, &native))); + sp_sys_file_meta_from_wasi(&native, st); + return SP_OK; #else #error "sp_sys_get_link_metadata" @@ -10242,12 +10616,13 @@ sp_err_t sp_sys_mkdir_p(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_per #elif defined(SP_MACOS) || defined(SP_COSMO) c8 buf [SP_PATH_MAX]; - sp_try(sp_sys_posix_path(path, sp_sys_posix_trim_slashes(path, len), buf)); + sp_try(sp_sys_posix_path(path, len, buf)); return sp_sys_err_from_libc(mkdirat(fd, buf, (mode_t)(perms.value & 07777))); #elif defined(SP_WASM) - (void)fd; (void)path; (void)len; (void)perms; - return SP_ERR_SYS_UNSUPPORTED; + (void)perms; + sp_try(sp_sys_wasi_path(path, &len)); + return sp_sys_err_from_wasi(sp_wasi_path_create_directory((__wasi_fd_t)fd, path, len)); #else #error "sp_sys_mkdir" @@ -10272,12 +10647,12 @@ sp_err_t sp_sys_rmdir_p(sp_sys_fd_t fd, const c8* path, u32 len) { #elif defined(SP_MACOS) || defined(SP_COSMO) c8 buf [SP_PATH_MAX]; - sp_try(sp_sys_posix_path(path, sp_sys_posix_trim_slashes(path, len), buf)); + sp_try(sp_sys_posix_path(path, len, buf)); return sp_sys_err_from_libc(unlinkat(fd, buf, AT_REMOVEDIR)); #elif defined(SP_WASM) - (void)fd; (void)path; (void)len; - return SP_ERR_SYS_UNSUPPORTED; + sp_try(sp_sys_wasi_path(path, &len)); + return sp_sys_err_from_wasi(sp_wasi_path_remove_directory((__wasi_fd_t)fd, path, len)); #else #error "sp_sys_rmdir" @@ -10293,14 +10668,7 @@ sp_err_t sp_sys_rmdir_s(sp_sys_fd_t fd, sp_str_t path) { /////////////////// sp_err_t sp_sys_unlink_p(sp_sys_fd_t fd, const c8* path, u32 len) { #if defined(SP_WIN32) - sp_str_t full = sp_str(path, len); - sp_str_t trimmed = sp_fs_trim_path(full); - if (trimmed.len != full.len) { - sp_sys_file_meta_t meta = sp_zero; - sp_try(sp_sys_file_meta_from_nt_path(fd, trimmed, &meta, false)); - return meta.kind == SP_FS_KIND_DIR ? SP_ERR_SYS_IS_DIR : SP_ERR_SYS_NOT_DIR; - } - return sp_sys_nt_delete(fd, full, false); + return sp_sys_nt_delete(fd, sp_str(path, len), false); #elif defined(SP_LINUX) c8 buf [SP_PATH_MAX]; @@ -10309,14 +10677,7 @@ sp_err_t sp_sys_unlink_p(sp_sys_fd_t fd, const c8* path, u32 len) { #elif defined(SP_MACOS) || defined(SP_COSMO) c8 buf [SP_PATH_MAX]; - u32 n = sp_sys_posix_trim_slashes(path, len); - sp_try(sp_sys_posix_path(path, n, buf)); - - if (n != len) { - struct stat st; - sp_try(sp_sys_err_from_libc(fstatat(fd, buf, &st, AT_SYMLINK_NOFOLLOW))); - return S_ISDIR(st.st_mode) ? SP_ERR_SYS_IS_DIR : SP_ERR_SYS_NOT_DIR; - } + sp_try(sp_sys_posix_path(path, len, buf)); s32 rc = unlinkat(fd, buf, 0); if (rc < 0 && errno == SP_EPERM) { @@ -10330,8 +10691,8 @@ sp_err_t sp_sys_unlink_p(sp_sys_fd_t fd, const c8* path, u32 len) { return sp_sys_err_from_libc(rc); #elif defined(SP_WASM) - (void)fd; (void)path; (void)len; - return SP_ERR_SYS_UNSUPPORTED; + sp_try(sp_sys_wasi_path(path, &len)); + return sp_sys_err_from_wasi(sp_wasi_path_unlink_file((__wasi_fd_t)fd, path, len)); #else #error "sp_sys_unlink" @@ -10347,6 +10708,7 @@ sp_err_t sp_sys_unlink_s(sp_sys_fd_t fd, sp_str_t path) { ////////////////// sp_err_t sp_sys_chdir_p(const c8* path, u32 len) { #if defined(SP_WIN32) + len = sp_sys_trim_path(path, len); if (!len) return SP_ERR_SYS_NOT_FOUND; if (len >= SP_PATH_MAX) return SP_ERR_SYS_NAME_TOO_LONG; SP_ALIGNED u16 wbuf[SP_PATH_MAX + 1]; @@ -10416,8 +10778,9 @@ sp_err_t sp_sys_link_p(sp_sys_fd_t from_fd, const c8* existing, u32 existing_len return sp_sys_err_from_libc(linkat(from_fd, buffers.existing, to_fd, buffers.alias, 0)); #elif defined(SP_WASM) - (void)from_fd; (void)existing; (void)existing_len; (void)to_fd; (void)alias; (void)alias_len; - return SP_ERR_SYS_UNSUPPORTED; + sp_try(sp_sys_wasi_path(existing, &existing_len)); + sp_try(sp_sys_wasi_path(alias, &alias_len)); + return sp_sys_err_from_wasi(sp_wasi_path_link((__wasi_fd_t)from_fd, 0, existing, existing_len, (__wasi_fd_t)to_fd, alias, alias_len)); #else #error "sp_sys_link" @@ -10431,7 +10794,14 @@ sp_err_t sp_sys_link_s(sp_sys_fd_t from_fd, sp_str_t existing, sp_sys_fd_t to_fd //////////////////// // SP_SYS_SYMLINK // //////////////////// -sp_err_t sp_sys_symlink_p(const c8* existing, u32 existing_len, sp_sys_fd_t to_fd, const c8* alias, u32 alias_len) { +sp_err_t sp_sys_symlink_p(const c8* existing, u32 existing_len, sp_sys_fd_t to_fd, const c8* alias, u32 alias_len, sp_fs_kind_t kind) { + switch (kind) { + case SP_FS_KIND_FILE: + case SP_FS_KIND_DIR: break; + case SP_FS_KIND_NONE: + case SP_FS_KIND_SYMLINK: return SP_ERR_SYS_INVALID; + } + #if defined(SP_WIN32) sp_str_t target = sp_str(existing, existing_len); sp_str_t link = sp_str(alias, alias_len); @@ -10452,26 +10822,7 @@ sp_err_t sp_sys_symlink_p(const c8* existing, u32 existing_len, sp_sys_fd_t to_f (wtarget.len >= 3 && wt[1] == ':' && wt[2] == '\\') || (wtarget.len >= 2 && wt[0] == '\\' && wt[1] == '\\'); - u32 dir_option = SP_NT_FILE_NON_DIRECTORY_FILE; - { - c8 probe_buf [SP_PATH_MAX]; - sp_str_t probe = target; - if (!absolute) { - u32 parent_len = 0; - sp_for(it, link.len) { - if (link.data[it] == '/' || link.data[it] == '\\') parent_len = (u32)it + 1; - } - if ((u64)parent_len + target.len <= sizeof(probe_buf)) { - sp_mem_copy(probe_buf, link.data, parent_len); - sp_mem_copy(probe_buf + parent_len, target.data, target.len); - probe = sp_str(probe_buf, parent_len + target.len); - } - } - sp_sys_file_meta_t meta = sp_zero; - if (!sp_sys_file_meta_from_nt_path(to_fd, probe, &meta, true) && meta.kind == SP_FS_KIND_DIR) { - dir_option = SP_NT_FILE_DIRECTORY_FILE; - } - } + u32 dir_option = kind == SP_FS_KIND_DIR ? SP_NT_FILE_DIRECTORY_FILE : SP_NT_FILE_NON_DIRECTORY_FILE; sp_wide_str_t stored = wtarget; sp_sys_nt_path_t nt_path = sp_zero; @@ -10536,7 +10887,7 @@ sp_err_t sp_sys_symlink_p(const c8* existing, u32 existing_len, sp_sys_fd_t to_f c8 existing [SP_PATH_MAX]; c8 alias [SP_PATH_MAX]; } buffers; - sp_try(sp_sys_posix_path(existing, existing_len, buffers.existing)); + sp_try(sp_sys_posix_cstr(existing, existing_len, buffers.existing)); sp_try(sp_sys_posix_path(alias, alias_len, buffers.alias)); return sp_syscall_e(SP_SYSCALL_NUM_SYMLINKAT, buffers.existing, to_fd, buffers.alias); @@ -10545,21 +10896,23 @@ sp_err_t sp_sys_symlink_p(const c8* existing, u32 existing_len, sp_sys_fd_t to_f c8 existing [SP_PATH_MAX]; c8 alias [SP_PATH_MAX]; } buffers; - sp_try(sp_sys_posix_path(existing, existing_len, buffers.existing)); + sp_try(sp_sys_posix_cstr(existing, existing_len, buffers.existing)); sp_try(sp_sys_posix_path(alias, alias_len, buffers.alias)); return sp_sys_err_from_libc(symlinkat(buffers.existing, to_fd, buffers.alias)); #elif defined(SP_WASM) - (void)existing; (void)existing_len; (void)to_fd; (void)alias; (void)alias_len; - return SP_ERR_SYS_UNSUPPORTED; + if (!existing_len) return SP_ERR_SYS_NOT_FOUND; + if (existing_len >= SP_PATH_MAX) return SP_ERR_SYS_NAME_TOO_LONG; + sp_try(sp_sys_wasi_path(alias, &alias_len)); + return sp_sys_err_from_wasi(sp_wasi_path_symlink(existing, existing_len, (__wasi_fd_t)to_fd, alias, alias_len)); #else #error "sp_sys_symlink" #endif } -sp_err_t sp_sys_symlink_s(sp_str_t existing, sp_sys_fd_t to_fd, sp_str_t alias) { - return sp_sys_symlink(existing.data, existing.len, to_fd, alias.data, alias.len); +sp_err_t sp_sys_symlink_s(sp_str_t existing, sp_sys_fd_t to_fd, sp_str_t alias, sp_fs_kind_t kind) { + return sp_sys_symlink(existing.data, existing.len, to_fd, alias.data, alias.len, kind); } ///////////////////// @@ -10631,8 +10984,18 @@ sp_err_t sp_sys_readlink_p(sp_sys_fd_t fd, const c8* path, u32 len, c8* buf, u64 return SP_OK; #elif defined(SP_WASM) - (void)fd; (void)path; (void)len; (void)buf; (void)size; - return SP_ERR_SYS_UNSUPPORTED; + sp_try(sp_sys_wasi_path(path, &len)); + __wasi_size_t used = 0; + __wasi_errno_t err = sp_wasi_path_readlink((__wasi_fd_t)fd, path, len, (u8*)buf, (__wasi_size_t)size, &used); + switch (err) { + case SP_WASI_ESUCCESS: break; + case SP_WASI_EOVERFLOW: + case SP_WASI_ERANGE: return SP_ERR_SYS_NAME_TOO_LONG; + default: return sp_sys_err_from_wasi(err); + } + if ((u64)used == size) return SP_ERR_SYS_NAME_TOO_LONG; + *target_len = (u64)used; + return SP_OK; #else #error "sp_sys_readlink" @@ -10739,182 +11102,173 @@ sp_err_t sp_sys_set_times_s(sp_sys_fd_t fd, sp_str_t path, sp_sys_timespec_t ati return sp_sys_set_times(fd, path.data, path.len, atime, mtime); } -/////////////////////////////// -// SP_SYS_CANONICALIZE_PATH // -/////////////////////////////// -s64 sp_sys_canonicalize_path_p(const c8* path, u32 len, c8* buf, u64 size) { +//////////////////////// +// SP_SYS_GET_FD_PATH // +//////////////////////// #if defined(SP_WIN32) - if (!buf) return -1; - - sp_sys_fd_t handle = SP_SYS_INVALID_FD; - if (!SP_NT_SUCCESS(sp_sys_nt_open( - sp_sys_get_root(0), - sp_str(path, len), - FILE_READ_ATTRIBUTES | SYNCHRONIZE, - FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - SP_NT_FILE_OPEN, - SP_NT_FILE_SYNCHRONOUS_IO_NONALERT | SP_NT_FILE_OPEN_FOR_BACKUP_INTENT, - 0, - &handle - ))) { - return -1; - } +SP_PRIVATE sp_err_t sp_sys_get_handle_metadata(sp_sys_fd_t fd, sp_sys_file_meta_t* meta) { + return sp_sys_file_meta_from_nt_path(fd, sp_str_lit("."), meta, false); +} - u16 wbuf[SP_PATH_MAX]; - DWORD wlen = GetFinalPathNameByHandleW((HANDLE)handle, (LPWSTR)wbuf, SP_PATH_MAX, 0); - sp_sys_nt_close(handle); - if (wlen == 0 || wlen >= SP_PATH_MAX) return -1; +SP_PRIVATE sp_err_t sp_sys_read_fd_path(sp_sys_fd_t fd, c8 buf [SP_PATH_MAX], u64* len) { + u16 wbuf [SP_PATH_MAX]; + DWORD wlen = GetFinalPathNameByHandleW((HANDLE)fd, (LPWSTR)wbuf, SP_PATH_MAX, 0); + if (wlen == 0) return sp_sys_err_from_win32(GetLastError()); + if (wlen >= SP_PATH_MAX) return SP_ERR_SYS_NAME_TOO_LONG; sp_wide_str_t wide = sp_sys_dos_from_nt_wtf16(wbuf, wlen); - SP_ALIGNED c8 u8buf[SP_PATH_MAX * 3 + 1]; + SP_ALIGNED c8 u8buf [SP_PATH_MAX * 3 + 1]; sp_mem_fixed_t fixed = sp_mem_fixed(u8buf, sizeof(u8buf)); - sp_str_t utf8; - if (sp_wtf16_to_wtf8(sp_mem_fixed_as_allocator(&fixed), wide, &utf8) != SP_OK) return -1; - if (utf8.len >= size) return -1; - sp_mem_copy(buf, utf8.data, utf8.len); - buf[utf8.len] = 0; - return (s64)utf8.len; + sp_str_t utf8 = sp_zero; + sp_try(sp_wtf16_to_wtf8(sp_mem_fixed_as_allocator(&fixed), wide, &utf8)); + + s64 n = sp_sys_win32_copy_path(utf8, buf, SP_PATH_MAX); + if (n < 0) return SP_ERR_SYS_NAME_TOO_LONG; + *len = (u64)n; + return SP_OK; +} #elif defined(SP_LINUX) - c8 pbuf [SP_PATH_MAX]; - sp_try_as(sp_sys_posix_path(path, len, pbuf), -1); - s64 fd = sp_syscall(SP_SYSCALL_NUM_OPENAT, SP_AT_FDCWD, pbuf, SP_SYS_LINUX_O_PATH | SP_SYS_LINUX_O_CLOEXEC, 0); - if (fd < 0) return -1; +SP_PRIVATE sp_err_t sp_sys_get_handle_metadata(sp_sys_fd_t fd, sp_sys_file_meta_t* meta) { + return sp_sys_get_file_metadata_p(fd, meta); +} + +SP_PRIVATE sp_err_t sp_sys_read_fd_path(sp_sys_fd_t fd, c8 buf [SP_PATH_MAX], u64* len) { + if (fd == SP_AT_FDCWD) { + s64 rc = sp_syscall(SP_SYSCALL_NUM_GETCWD, buf, SP_PATH_MAX); + if (rc == -SP_ERANGE) return SP_ERR_SYS_NAME_TOO_LONG; + if (sp_sys_is_err(rc)) return sp_sys_err_from_errno(-rc); + *len = (u64)rc - 1; + return SP_OK; + } c8 proc [64] = sp_zero; sp_fmt_buf(proc, 64, "/proc/self/fd/{}", sp_fmt_int(fd)); - c8 rbuf [SP_PATH_MAX]; - s64 n = sp_syscall(SP_SYSCALL_NUM_READLINKAT, SP_AT_FDCWD, proc, rbuf, SP_PATH_MAX); - sp_sys_close(fd); - if (n < 0 || n == SP_PATH_MAX || (u64)n >= size) return -1; - sp_mem_copy(buf, rbuf, (u64)n); - buf[n] = 0; - return n; + s64 rc = sp_syscall(SP_SYSCALL_NUM_READLINKAT, SP_AT_FDCWD, proc, buf, SP_PATH_MAX); + if (sp_sys_is_err(rc)) return sp_sys_err_from_errno(-rc); + if (rc == SP_PATH_MAX) return SP_ERR_SYS_NAME_TOO_LONG; + *len = (u64)rc; + return SP_OK; +} #elif defined(SP_MACOS) || defined(SP_COSMO) - if (!path || !buf || size == 0) return -1; - c8 pbuf [SP_PATH_MAX]; - sp_try_as(sp_sys_posix_path(path, len, pbuf), -1); - c8 resolved [SP_PATH_MAX]; - if (!realpath(pbuf, resolved)) return -1; - u64 n = sp_cstr_len(resolved); - if (n >= size) return -1; - sp_mem_copy(buf, resolved, n); +SP_PRIVATE sp_err_t sp_sys_get_handle_metadata(sp_sys_fd_t fd, sp_sys_file_meta_t* meta) { + if (fd == SP_AT_FDCWD) return sp_sys_get_path_metadata_p(fd, ".", 1, meta); + return sp_sys_get_file_metadata_p(fd, meta); +} + +SP_PRIVATE sp_err_t sp_sys_read_fd_path(sp_sys_fd_t fd, c8 buf [SP_PATH_MAX], u64* len) { + if (fd == SP_AT_FDCWD) { + if (!getcwd(buf, SP_PATH_MAX)) return sp_sys_err_from_errno(errno); + } + else if (fcntl((s32)fd, F_GETPATH, buf) < 0) { + return sp_sys_err_from_errno(errno); + } + + *len = sp_cstr_len(buf); + return SP_OK; +} +#endif + +sp_err_t sp_sys_get_fd_path_p(sp_sys_fd_t fd, c8* buf, u64 size, u64* len) { + *len = 0; + +#if defined(SP_WIN32) || defined(SP_LINUX) || defined(SP_MACOS) || defined(SP_COSMO) + sp_sys_file_meta_t handle = sp_zero; + sp_err_t err = sp_sys_get_handle_metadata(fd, &handle); + if (err == SP_ERR_SYS_BAD_FD) return err; + if (err) return SP_ERR_SYS_NOT_FOUND; + + c8 path [SP_PATH_MAX]; + u64 n = 0; + err = sp_sys_read_fd_path(fd, path, &n); + if (err == SP_ERR_SYS_NAME_TOO_LONG) return err; + if (err) return SP_ERR_SYS_NOT_FOUND; + + sp_sys_file_meta_t named = sp_zero; + if (sp_sys_get_link_metadata_p(sp_sys_get_root_p(0), path, (u32)n, &named)) return SP_ERR_SYS_NOT_FOUND; + if (named.id != handle.id) return SP_ERR_SYS_NOT_FOUND; + if (named.device != handle.device) return SP_ERR_SYS_NOT_FOUND; + + if (n >= size) return SP_ERR_SYS_NAME_TOO_LONG; + sp_mem_copy(buf, path, n); buf[n] = 0; - return (s64)n; + *len = n; + return SP_OK; #elif defined(SP_WASM) - if (!path || !buf || size == 0) return -1; - u64 n = 0; - while (n < len && n < size - 1) { buf[n] = path[n]; n++; } - buf[n] = '\0'; - return (s64)n; + return SP_ERR_SYS_UNSUPPORTED; #else - #error "sp_sys_canonicalize_path" + #error "sp_sys_get_fd_path" #endif } -s64 sp_sys_canonicalize_path_s(sp_str_t path, c8* buf, u64 size) { - return sp_sys_canonicalize_path(path.data, path.len, buf, size); -} - ///////////////////////// // SP_SYS_GET_EXE_PATH // ///////////////////////// -s64 sp_sys_get_exe_path_p(c8* buf, u64 size) { -#if defined(SP_WIN32) - if (!buf) return -1; +sp_err_t sp_sys_get_exe_path_p(c8* buf, u64 size, u64* len) { + *len = 0; +#if defined(SP_WIN32) sp_nt_unicode_string_t* image = (sp_nt_unicode_string_t*)(sp_nt_process_params() + 0x60); u32 wlen = image->Length / (u32)sizeof(u16); SP_ALIGNED c8 u8buf[SP_PATH_MAX * 3 + 1]; sp_mem_fixed_t fixed = sp_mem_fixed(u8buf, sizeof(u8buf)); sp_str_t utf8; - if (sp_wtf16_to_wtf8(sp_mem_fixed_as_allocator(&fixed), (sp_wide_str_t) { .data = image->Buffer, .len = wlen }, &utf8) != SP_OK) return -1; - if (utf8.len >= size) return -1; - sp_mem_copy(buf, utf8.data, utf8.len); - buf[utf8.len] = 0; - return (s64)utf8.len; + sp_try(sp_wtf16_to_wtf8(sp_mem_fixed_as_allocator(&fixed), (sp_wide_str_t) { .data = image->Buffer, .len = wlen }, &utf8)); + + s64 n = sp_sys_win32_copy_path(utf8, buf, size); + if (n < 0) return SP_ERR_SYS_NAME_TOO_LONG; + *len = (u64)n; + return SP_OK; #elif defined(SP_LINUX) c8 rbuf [SP_PATH_MAX]; s64 n = sp_syscall(SP_SYSCALL_NUM_READLINKAT, SP_AT_FDCWD, "/proc/self/exe", rbuf, SP_PATH_MAX); - if (n < 0 || n == SP_PATH_MAX || (u64)n >= size) return -1; + if (n < 0) return sp_sys_err_from_errno(-n); + if (n == SP_PATH_MAX || (u64)n >= size) return SP_ERR_SYS_NAME_TOO_LONG; sp_mem_copy(buf, rbuf, (u64)n); buf[n] = 0; - return n; + *len = (u64)n; + return SP_OK; #elif defined(SP_MACOS) - if (!buf || size == 0) return -1; c8 raw[4096]; u32 raw_size = sizeof(raw); - if (_NSGetExecutablePath(raw, &raw_size)) return -1; - return sp_sys_canonicalize_path(raw, sp_cstr_len(raw), buf, size); + if (_NSGetExecutablePath(raw, &raw_size)) return SP_ERR_SYS_NAME_TOO_LONG; + + c8 resolved [SP_PATH_MAX]; + if (!realpath(raw, resolved)) return sp_sys_err_from_errno(errno); + u64 n = sp_cstr_len(resolved); + if (n >= size) return SP_ERR_SYS_NAME_TOO_LONG; + sp_mem_copy(buf, resolved, n); + buf[n] = 0; + *len = n; + return SP_OK; #elif defined(SP_COSMO) - if (!buf || size == 0) return -1; extern char* program_invocation_name; - return sp_sys_canonicalize_path(program_invocation_name, sp_cstr_len(program_invocation_name), buf, size); + + c8 resolved [SP_PATH_MAX]; + if (!realpath(program_invocation_name, resolved)) return sp_sys_err_from_errno(errno); + u64 n = sp_cstr_len(resolved); + if (n >= size) return SP_ERR_SYS_NAME_TOO_LONG; + sp_mem_copy(buf, resolved, n); + buf[n] = 0; + *len = n; + return SP_OK; #elif defined(SP_WASM) (void)buf; (void)size; - return -1; + return SP_ERR_SYS_UNSUPPORTED; #else #error "sp_sys_get_exe_path" #endif } -///////////////////////// -// SP_SYS_GET_CWD_PATH // -///////////////////////// -s64 sp_sys_get_cwd_path_p(c8* buf, u64 size) { -#if defined(SP_WIN32) - if (!buf) return -1; - - sp_nt_unicode_string_t* cwd = (sp_nt_unicode_string_t*)(sp_nt_process_params() + 0x38); - u32 wlen = cwd->Length / (u32)sizeof(u16); - SP_ALIGNED c8 u8buf[SP_PATH_MAX * 3 + 1]; - sp_mem_fixed_t fixed = sp_mem_fixed(u8buf, sizeof(u8buf)); - sp_str_t utf8; - if (sp_wtf16_to_wtf8(sp_mem_fixed_as_allocator(&fixed), (sp_wide_str_t) { .data = cwd->Buffer, .len = wlen }, &utf8) != SP_OK) return -1; - - // The PEB DosPath always carries a trailing separator; strip it the way - // GetCurrentDirectoryW does, except at a drive root, where "C:" would mean - // something else (drive-relative). - c8 last = utf8.len ? utf8.data[utf8.len - 1] : 0; - if (utf8.len > 1 && (last == '\\' || last == '/') && !(utf8.len == 3 && utf8.data[1] == ':')) { - utf8.len--; - } - - if (utf8.len >= size) return -1; - sp_mem_copy(buf, utf8.data, utf8.len); - buf[utf8.len] = 0; - return (s64)utf8.len; - -#elif defined(SP_LINUX) - s64 n = sp_syscall(SP_SYSCALL_NUM_GETCWD, buf, size); - if (n <= 0) return -1; - return n - 1; - -#elif defined(SP_MACOS) || defined(SP_COSMO) - if (!buf || size == 0) return -1; - if (!getcwd(buf, size)) return -1; - return (s64)sp_cstr_len(buf); - -#elif defined(SP_WASM) - if (!buf || size < 2) return -1; - buf[0] = '.'; - buf[1] = 0; - return 1; - -#else - #error "sp_sys_get_cwd_path" -#endif -} - ///////////////////// // SP_OS_GET_NAME // ///////////////////// @@ -13489,34 +13843,46 @@ SP_PRIVATE sp_fs_kind_t sp_sys_dir_dtype_to_kind(u8 d_type) { } return SP_FS_KIND_NONE; } + +#elif defined(SP_WASM) +sp_static_assert(SP_SYS_DIR_MIN_BUF >= sizeof(__wasi_dirent_t) + 255, sp_sys_dir_min_buf); + +SP_PRIVATE sp_fs_kind_t sp_sys_dir_dtype_to_kind(u8 d_type) { + switch (d_type) { + case __WASI_FILETYPE_REGULAR_FILE: { return SP_FS_KIND_FILE; } + case __WASI_FILETYPE_DIRECTORY: { return SP_FS_KIND_DIR; } + case __WASI_FILETYPE_SYMBOLIC_LINK: { return SP_FS_KIND_SYMLINK; } + } + return SP_FS_KIND_NONE; +} #endif -sp_err_t sp_sys_dir_from_fd_p(sp_sys_fd_t fd, sp_sys_dir_t* out) { - *out = sp_zero_s(sp_sys_dir_t); +sp_err_t sp_sys_dir_it_open_p(sp_sys_fd_t fd, sp_sys_dir_it_t* out) { + *out = sp_zero_s(sp_sys_dir_it_t); + out->fd = fd; #if defined(SP_WIN32) || defined(SP_LINUX) - out->handle = (s64)fd; return SP_OK; #elif defined(SP_MACOS) || defined(SP_COSMO) DIR* dir = fdopendir((int)fd); if (!dir) return sp_sys_err_from_errno(errno); - out->handle = (s64)(intptr_t)dir; + out->state = (s64)(intptr_t)dir; return SP_OK; #elif defined(SP_WASM) - (void)fd; - return SP_ERR_SYS_UNSUPPORTED; + out->state = (s64)__WASI_DIRCOOKIE_START; + return SP_OK; #else - #error "sp_sys_dir_from_fd" + #error "sp_sys_dir_it_open" #endif } -sp_err_t sp_sys_dir_read_p(sp_sys_dir_t* dir, sp_mem_buffer_t* buf) { +sp_err_t sp_sys_dir_it_read_p(sp_sys_dir_it_t* it, sp_mem_buffer_t* buf) { buf->len = 0; #if defined(SP_WIN32) sp_nt_io_status_block_t iosb = sp_zero; sp_nt_status_t status = SP_NT(NtQueryDirectoryFile)( - (void*)(intptr_t)dir->handle, + (void*)(intptr_t)it->fd, SP_NULLPTR, SP_NULLPTR, SP_NULLPTR, &iosb, buf->data, (u32)(buf->capacity - SP_SYS_DIR_WIN32_SCRATCH), @@ -13528,30 +13894,44 @@ sp_err_t sp_sys_dir_read_p(sp_sys_dir_t* dir, sp_mem_buffer_t* buf) { buf->len = (u64)iosb.Information; return SP_OK; #elif defined(SP_LINUX) - s64 n = sp_lx_getdents64((sp_sys_fd_t)dir->handle, buf->data, buf->capacity); + s64 n = sp_lx_getdents64(it->fd, buf->data, buf->capacity); if (n == -SP_ENOENT) return SP_OK; if (n < 0) return sp_sys_err_from_errno(-n); buf->len = (u64)n; return SP_OK; #elif defined(SP_MACOS) || defined(SP_COSMO) errno = 0; - struct dirent* d = readdir((DIR*)(intptr_t)dir->handle); + struct dirent* d = readdir((DIR*)(intptr_t)it->state); if (!d) return errno ? sp_sys_err_from_errno(errno) : SP_OK; sp_mem_copy(buf->data, d, d->d_reclen); buf->len = d->d_reclen; return SP_OK; #elif defined(SP_WASM) - (void)dir; - return SP_ERR_SYS_UNSUPPORTED; + __wasi_size_t used = 0; + __wasi_errno_t err = sp_wasi_fd_readdir((__wasi_fd_t)it->fd, buf->data, (__wasi_size_t)buf->capacity, (__wasi_dircookie_t)it->state, &used); + if (err == SP_WASI_ENOENT) return SP_OK; + if (err) return sp_sys_err_from_wasi(err); + + u64 end = 0; + while (end + sizeof(__wasi_dirent_t) <= used) { + __wasi_dirent_t d; + sp_mem_copy(&d, buf->data + end, sizeof(d)); + if (end + sizeof(d) + d.d_namlen > used) break; + end += sizeof(d) + d.d_namlen; + it->state = (s64)d.d_next; + } + if (used && !end) return SP_ERR_SYS_NAME_TOO_LONG; + buf->len = end; + return SP_OK; #else - #error "sp_sys_dir_read" + #error "sp_sys_dir_it_read" #endif } -sp_err_t sp_sys_dir_parse_p(sp_sys_dir_t* dir, sp_mem_buffer_t* buf, u64* cursor, sp_sys_dir_entry_t* out) { +sp_err_t sp_sys_dir_it_parse_p(sp_sys_dir_it_t* it, sp_mem_buffer_t* buf, u64* cursor, sp_sys_dir_entry_t* out) { *out = sp_zero_s(sp_sys_dir_entry_t); #if defined(SP_WIN32) - (void)dir; + (void)it; sp_nt_file_directory_information_t* d = sp_ptr_cast(sp_nt_file_directory_information_t*, buf->data + *cursor); *cursor = d->NextEntryOffset ? *cursor + d->NextEntryOffset : buf->len; @@ -13565,7 +13945,7 @@ sp_err_t sp_sys_dir_parse_p(sp_sys_dir_t* dir, sp_mem_buffer_t* buf, u64* cursor out->kind = sp_sys_dir_win32_attrs(d->FileAttributes); return SP_OK; #elif defined(SP_LINUX) - (void)dir; + (void)it; sp_sys_dirent64_t* d = sp_ptr_cast(sp_sys_dirent64_t*, buf->data + *cursor); *cursor += d->d_reclen; @@ -13574,7 +13954,7 @@ sp_err_t sp_sys_dir_parse_p(sp_sys_dir_t* dir, sp_mem_buffer_t* buf, u64* cursor out->kind = sp_sys_dir_dtype_to_kind(d->d_type); return SP_OK; #elif defined(SP_MACOS) || defined(SP_COSMO) - (void)dir; + (void)it; struct dirent* d = sp_ptr_cast(struct dirent*, buf->data + *cursor); *cursor += d->d_reclen; @@ -13583,24 +13963,29 @@ sp_err_t sp_sys_dir_parse_p(sp_sys_dir_t* dir, sp_mem_buffer_t* buf, u64* cursor out->kind = sp_sys_dir_dtype_to_kind(d->d_type); return SP_OK; #elif defined(SP_WASM) - (void)dir; (void)buf; (void)cursor; - return SP_ERR_SYS_UNSUPPORTED; + (void)it; + __wasi_dirent_t d; + sp_mem_copy(&d, buf->data + *cursor, sizeof(d)); + *cursor += sizeof(d); + + out->name = sp_ptr_cast(const c8*, buf->data + *cursor); + out->len = d.d_namlen; + out->kind = sp_sys_dir_dtype_to_kind(d.d_type); + *cursor += d.d_namlen; + return SP_OK; #else - #error "sp_sys_dir_parse" + #error "sp_sys_dir_it_parse" #endif } -sp_err_t sp_sys_dir_close_p(sp_sys_dir_t* dir) { -#if defined(SP_WIN32) || defined(SP_LINUX) - return sp_sys_close((sp_sys_fd_t)dir->handle); +sp_err_t sp_sys_dir_it_close_p(sp_sys_dir_it_t* it) { +#if defined(SP_WIN32) || defined(SP_LINUX) || defined(SP_WASM) + return sp_sys_close(it->fd); #elif defined(SP_MACOS) || defined(SP_COSMO) - if (closedir((DIR*)(intptr_t)dir->handle)) return sp_sys_err_from_errno(errno); + if (closedir((DIR*)(intptr_t)it->state)) return sp_sys_err_from_errno(errno); return SP_OK; -#elif defined(SP_WASM) - (void)dir; - return SP_ERR_SYS_UNSUPPORTED; #else - #error "sp_sys_dir_close" + #error "sp_sys_dir_it_close" #endif } @@ -16563,7 +16948,7 @@ void sp_fmon_os_add_dir(sp_fmon_t* monitor, sp_str_t path) { sp_sys_fd_t handle = SP_SYS_INVALID_FD; if (!SP_NT_SUCCESS(sp_sys_nt_open( - sp_sys_get_root(0), + sp_fs_get_cwd(), path, FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, @@ -16579,7 +16964,11 @@ void sp_fmon_os_add_dir(sp_fmon_t* monitor, sp_str_t path) { sp_fmon_dir_t dir = sp_zero; dir.overlapped.hEvent = event; dir.handle = (sp_win32_handle_t)handle; - dir.path = sp_fs_canonicalize_path(monitor->mem, path); + if (sp_fs_canonicalize_path(monitor->mem, path, &dir.path)) { + CloseHandle(dir.handle); + CloseHandle(event); + return; + } dir.notify_information = sp_alloc(monitor->mem, SP_FILE_MONITOR_BUFFER_SIZE); sp_mem_zero(dir.notify_information, SP_FILE_MONITOR_BUFFER_SIZE); @@ -16590,14 +16979,16 @@ void sp_fmon_os_add_dir(sp_fmon_t* monitor, sp_str_t path) { void sp_fmon_os_add_file(sp_fmon_t* monitor, sp_str_t file_path) { sp_fmon_os_t* os = monitor->os; - sp_str_t canonical = sp_fs_canonicalize_path(monitor->mem, file_path); + sp_str_t canonical = sp_zero; + if (sp_fs_canonicalize_path(monitor->mem, file_path, &canonical)) return; sp_da_push(os->watch_files, canonical); sp_str_t dir_path = sp_fs_parent_path(canonical); if (dir_path.len > 0) { bool found = false; sp_da_for(os->dirs, i) { - sp_str_t path = sp_fs_canonicalize_path(monitor->mem, os->dirs[i].path); // @spader + sp_str_t path = sp_zero; + if (sp_fs_canonicalize_path(monitor->mem, os->dirs[i].path, &path)) continue; // @spader if (sp_str_equal(path, dir_path)) { found = true; break; @@ -16774,7 +17165,8 @@ void sp_fmon_os_deinit(sp_fmon_t* monitor) { void sp_fmon_os_add_file(sp_fmon_t* monitor, sp_str_t file_path) { sp_fmon_os_t* os = (sp_fmon_os_t*)monitor->os; - sp_str_t canonical = sp_fs_canonicalize_path(monitor->mem, file_path); + sp_str_t canonical = sp_zero; + if (sp_fs_canonicalize_path(monitor->mem, file_path, &canonical)) return; sp_str_ht_insert(os->files, canonical, 1); sp_str_t dir_path = sp_fs_parent_path(canonical); @@ -17023,7 +17415,7 @@ void sp_fmon_os_push_dir(sp_fmon_os_t* os, sp_str_t dir) { void sp_fmon_os_push_file(sp_fmon_os_t* os, sp_str_t file) { sp_mutex_lock(&os->mutex); - sp_da_push(os->watch_files, sp_fs_canonicalize_path(os->mem.watch, file)); + sp_da_push(os->watch_files, sp_str_copy(os->mem.watch, file)); sp_mutex_unlock(&os->mutex); } @@ -17035,30 +17427,34 @@ void sp_fmon_os_add_dir(sp_fmon_t* monitor, sp_str_t path) { void sp_fmon_os_add_file(sp_fmon_t* monitor, sp_str_t path) { sp_fmon_os_t* os = monitor->os; - sp_fmon_os_push_file(os, path); + sp_mem_arena_marker_t scratch = sp_mem_begin_scratch(); - { - sp_mem_arena_marker_t scratch = sp_mem_begin_scratch(); + sp_str_t canonical = sp_zero; + if (sp_fs_canonicalize_path(scratch.mem, path, &canonical)) { + sp_mem_end_scratch(scratch); + return; + } - sp_str_t dir = sp_fs_parent_path(sp_fs_canonicalize_path(path)); + sp_fmon_os_push_file(os, canonical); - bool found = false; - sp_mutex_lock(&os->mutex); - sp_da_for(os->watch_paths, it) { - if (sp_str_equal(os->watch_paths[it], dir)) { - found = true; - break; - } - } - sp_mutex_unlock(&os->mutex); + sp_str_t dir = sp_fs_parent_path(canonical); - if (!found) { - sp_fmon_os_push_dir(os, dir); - sp_fmon_fsevents_recreate_stream(monitor); + bool found = false; + sp_mutex_lock(&os->mutex); + sp_da_for(os->watch_paths, it) { + if (sp_str_equal(os->watch_paths[it], dir)) { + found = true; + break; } + } + sp_mutex_unlock(&os->mutex); - sp_mem_end_scratch(scratch); + if (!found) { + sp_fmon_os_push_dir(os, dir); + sp_fmon_fsevents_recreate_stream(monitor); } + + sp_mem_end_scratch(scratch); } void sp_fmon_os_process_changes(sp_fmon_t* monitor) { @@ -17314,9 +17710,9 @@ sp_err_t sp_io_file_reader_from_file(sp_io_file_reader_t* r, sp_sys_fd_t file, s return SP_OK; } -sp_err_t sp_io_file_reader_from_path(sp_io_file_reader_t* r, sp_str_t path) { +sp_err_t sp_io_file_reader_from_path_at(sp_io_file_reader_t* r, sp_path_t path) { sp_sys_fd_t fd = SP_SYS_INVALID_FD; - sp_err_t err = sp_sys_open_s(sp_sys_get_root(0), path, SP_SYS_OPEN_MODE_RO, 0, &fd); + sp_err_t err = sp_sys_open_s(path.dir, path.sub, SP_SYS_OPEN_MODE_RO, 0, &fd); if (err) { *r = sp_zero_s(sp_io_file_reader_t); return err; @@ -17324,6 +17720,10 @@ sp_err_t sp_io_file_reader_from_path(sp_io_file_reader_t* r, sp_str_t path) { return sp_io_file_reader_from_file(r, fd, SP_IO_CLOSE_MODE_AUTO); } +sp_err_t sp_io_file_reader_from_path(sp_io_file_reader_t* r, sp_str_t path) { + return sp_io_file_reader_from_path_at(r, sp_path_cwd(path)); +} + sp_err_t sp_io_file_reader_seek(sp_io_file_reader_t* r, s64 offset, sp_io_whence_t whence, s64* position) { sp_err_t err = SP_OK; s64 pos = 0; @@ -17983,10 +18383,10 @@ sp_err_t sp_io_file_writer_from_fd(sp_io_file_writer_t* w, sp_sys_fd_t fd, sp_io return SP_OK; } -sp_err_t sp_io_file_writer_from_path(sp_io_file_writer_t* w, sp_str_t path) { +sp_err_t sp_io_file_writer_from_path_at(sp_io_file_writer_t* w, sp_path_t path) { u32 flags = SP_SYS_OPEN_CREATE | SP_SYS_OPEN_TRUNCATE; sp_sys_fd_t fd = SP_SYS_INVALID_FD; - sp_err_t err = sp_sys_open_s(sp_sys_get_root(0), path, SP_SYS_OPEN_MODE_WO, flags, &fd); + sp_err_t err = sp_sys_open_s(path.dir, path.sub, SP_SYS_OPEN_MODE_WO, flags, &fd); if (err) { *w = sp_zero_s(sp_io_file_writer_t); return err; @@ -17997,6 +18397,10 @@ sp_err_t sp_io_file_writer_from_path(sp_io_file_writer_t* w, sp_str_t path) { return SP_OK; } +sp_err_t sp_io_file_writer_from_path(sp_io_file_writer_t* w, sp_str_t path) { + return sp_io_file_writer_from_path_at(w, sp_path_cwd(path)); +} + sp_err_t sp_io_writer_set_buffer(sp_io_writer_t* writer, u8* ptr, u64 size) { sp_assert(writer); sp_try(sp_io_flush(writer)); @@ -19704,98 +20108,126 @@ sp_str_r sp_fmt_write_f64_buf(c8* buffer, u64 len, f64 value, u32 precision) { return str; } -sp_str_t sp_fs_get_name(sp_str_t path) { - // The only valid separator is '/'. If you're calling this function on a path - // that might contain a '\', normalize it first. Garbage in, garbage out. - s32 it = sp_str_find_c8_reverse(path, '/'); - return sp_str_suffix(path, ((s32)path.len) - it - 1); +bool sp_fs_is_sep_for(c8 c, sp_fs_path_kind_t kind) { + switch (kind) { + case SP_FS_PATH_POSIX: return c == '/'; + case SP_FS_PATH_WINDOWS: return c == '/' || c == '\\'; + } + SP_UNREACHABLE_RETURN(false); } -sp_str_t sp_fs_trim_path(sp_str_t path) { - if (sp_str_empty(path)) return path; +SP_PRIVATE u32 sp_fs_root_len(sp_str_t path, sp_fs_path_kind_t kind) { + if (sp_str_empty(path)) return 0; + if (sp_fs_is_sep_for(path.data[0], kind)) return 1; - while (!sp_str_empty(path) && !sp_fs_is_root(path)) { - switch (sp_str_back(path)) { - case '/': - case '\\': { - path.len--; - break; - } - default: { - return path; - } - } + switch (kind) { + case SP_FS_PATH_POSIX: return 0; + case SP_FS_PATH_WINDOWS: return path.len >= 3 && path.data[1] == ':' && sp_fs_is_sep_for(path.data[2], kind) ? 3 : 0; } + SP_UNREACHABLE_RETURN(0); +} - return path; +SP_PRIVATE bool sp_fs_is_dot(sp_str_t name) { + return sp_str_equal(name, sp_str_lit(".")) || sp_str_equal(name, sp_str_lit("..")); } -sp_str_t sp_fs_parent_path(sp_str_t path) { - if (sp_str_empty(path)) return path; - if (sp_fs_is_root(path)) return path; +SP_PRIVATE u32 sp_fs_stem_len(sp_str_t name) { + u32 hidden = 0; + while (hidden < name.len && name.data[hidden] == '.') hidden++; - path = sp_fs_trim_path(path); + for (u32 it = name.len; it > hidden; it--) { + if (name.data[it - 1] == '.') return it - 1; + } + return name.len; +} - s32 index = sp_str_find_c8_reverse(path, '/'); - return index == SP_STR_NO_MATCH ? sp_str_lit("") : sp_str_prefix(path, index); +bool sp_fs_is_absolute_for(sp_str_t path, sp_fs_path_kind_t kind) { + return sp_fs_root_len(path, kind) > 0; } -sp_str_t sp_fs_get_ext(sp_str_t path) { - for (u32 index = 0; index < path.len; index++) { - c8 c = sp_str_at_reverse(path, index); +sp_str_t sp_fs_trim_path_for(sp_str_t path, sp_fs_path_kind_t kind) { + u32 root = sp_fs_root_len(path, kind); + while (path.len > root && sp_fs_is_sep_for(sp_str_back(path), kind)) path.len--; + return path; +} - switch (c) { - case '.': return sp_str_sub_reverse(path, 0, index); - case '/': return sp_str_sub_reverse(path, 0, 0); - default: break; - } - } +bool sp_fs_is_root_for(sp_str_t path, sp_fs_path_kind_t kind) { + u32 root = sp_fs_root_len(path, kind); + return root && sp_fs_trim_path_for(path, kind).len == root; +} - return sp_str_sub_reverse(path, 0, 0); +sp_str_t sp_fs_get_name_for(sp_str_t path, sp_fs_path_kind_t kind) { + path = sp_fs_trim_path_for(path, kind); + + u32 root = sp_fs_root_len(path, kind); + u32 start = path.len; + while (start > root && !sp_fs_is_sep_for(path.data[start - 1], kind)) start--; + return sp_str_suffix(path, (s32)(path.len - start)); } -sp_str_t sp_fs_get_stem(sp_str_t path) { - sp_str_t file_name = sp_fs_get_name(path); - if (!file_name.len) return path; +sp_str_t sp_fs_parent_path_for(sp_str_t path, sp_fs_path_kind_t kind) { + path = sp_fs_trim_path_for(path, kind); + if (sp_str_empty(path)) return path; - sp_str_t extension = sp_fs_get_ext(path); + sp_str_t name = sp_fs_get_name_for(path, kind); + sp_str_t parent = sp_fs_trim_path_for(sp_str_prefix(path, (s32)(path.len - name.len)), kind); + return sp_str_empty(parent) ? sp_str_lit(".") : parent; +} - sp_str_t stem = { - .data = file_name.data, - .len = file_name.len - extension.len, - }; +sp_str_t sp_fs_get_stem_for(sp_str_t path, sp_fs_path_kind_t kind) { + sp_str_t name = sp_fs_get_name_for(path, kind); + return sp_str_prefix(name, (s32)sp_fs_stem_len(name)); +} + +sp_str_t sp_fs_get_ext_for(sp_str_t path, sp_fs_path_kind_t kind) { + sp_str_t name = sp_fs_get_name_for(path, kind); + u32 dot = sp_fs_stem_len(name); + return sp_str_suffix(name, (s32)(name.len - sp_min(dot + 1, name.len))); +} - if (sp_str_back(stem) == '.') stem.len--; +sp_str_t sp_fs_replace_ext_for(sp_mem_t mem, sp_str_t path, sp_str_t ext, sp_fs_path_kind_t kind) { + path = sp_fs_trim_path_for(path, kind); - return stem; + sp_str_t name = sp_fs_get_name_for(path, kind); + sp_str_t base = sp_str_prefix(path, (s32)(path.len - name.len + sp_fs_stem_len(name))); + return sp_str_empty(ext) ? + sp_str_copy(mem, base) : + sp_str_join(mem, base, ext, sp_str_lit(".")); } -bool sp_fs_is_sep(c8 c) { - return c == '/' || c == '\\'; +sp_str_t sp_fs_join_path_for(sp_mem_t mem, sp_str_t a, sp_str_t b, sp_fs_path_kind_t kind) { + a = sp_fs_trim_path_for(a, kind); + b = sp_fs_trim_path_for(b, kind); + if (sp_str_empty(a)) return sp_str_copy(mem, b); + + while (!sp_str_empty(b) && sp_fs_is_sep_for(b.data[0], kind)) b = sp_str_suffix(b, (s32)b.len - 1); + if (sp_str_empty(b)) return sp_str_copy(mem, a); + if (sp_fs_is_sep_for(sp_str_back(a), kind)) return sp_str_concat(mem, a, b); + return sp_str_join(mem, a, b, sp_str_lit("/")); } -bool sp_fs_is_absolute_for(sp_str_t path, sp_fs_path_kind_t kind) { - if (path.len == 0) return false; +sp_str_t sp_fs_normalize_path_for(sp_mem_t mem, sp_str_t path, sp_fs_path_kind_t kind) { + path = sp_fs_trim_path_for(path, kind); - if (sp_fs_is_sep(path.data[0])) { - return true; + c8* buffer = sp_alloc_n(mem, c8, path.len); + sp_for(it, path.len) { + buffer[it] = sp_fs_is_sep_for(path.data[it], kind) ? '/' : path.data[it]; } + return sp_str(buffer, path.len); +} - if (kind != SP_FS_PATH_WINDOWS) return false; +bool sp_fs_is_sep(c8 c) { + return sp_fs_is_sep_for(c, sp_os_get_path_kind()); +} - return (path.len >= 3 && path.data[1] == ':' && sp_fs_is_sep(path.data[2])); +bool sp_fs_is_root(sp_str_t path) { + return sp_fs_is_root_for(path, sp_os_get_path_kind()); } bool sp_fs_is_absolute(sp_str_t path) { return sp_fs_is_absolute_for(path, sp_os_get_path_kind()); } -bool sp_fs_is_root(sp_str_t path) { - if (path.len == 1 && sp_fs_is_sep(path.data[0])) return true; - if (path.len == 3 && path.data[1] == ':' && sp_fs_is_sep(path.data[2])) return true; - return false; -} - bool sp_fs_is_absolute_w(sp_wide_str_t path) { if (path.len == 0) return false; if (path.data[0] == '/' || path.data[0] == '\\') return true; @@ -19804,167 +20236,240 @@ bool sp_fs_is_absolute_w(sp_wide_str_t path) { return false; } -bool sp_fs_is_glob(sp_str_t path) { - return sp_str_find_c8(path, '*') != SP_STR_NO_MATCH; +sp_str_t sp_fs_trim_path(sp_str_t path) { + return sp_fs_trim_path_for(path, sp_os_get_path_kind()); } -sp_str_t sp_fs_normalize_path(sp_mem_t mem, sp_str_t path) { - if (sp_str_back(path) == '/' || sp_str_back(path) == '\\') { - path.len--; - } +sp_str_t sp_fs_get_name(sp_str_t path) { + return sp_fs_get_name_for(path, sp_os_get_path_kind()); +} - c8* buffer = (c8*)sp_mem_allocator_alloc(mem, path.len); - sp_for(i, path.len) { - c8 c = path.data[i]; - buffer[i] = (c == '\\' ? '/' : c); - } - return (sp_str_t) { .data = buffer, .len = path.len }; +sp_str_t sp_fs_parent_path(sp_str_t path) { + return sp_fs_parent_path_for(path, sp_os_get_path_kind()); +} + +sp_str_t sp_fs_get_stem(sp_str_t path) { + return sp_fs_get_stem_for(path, sp_os_get_path_kind()); +} + +sp_str_t sp_fs_get_ext(sp_str_t path) { + return sp_fs_get_ext_for(path, sp_os_get_path_kind()); +} + +sp_str_t sp_fs_replace_ext(sp_mem_t mem, sp_str_t path, sp_str_t ext) { + return sp_fs_replace_ext_for(mem, path, ext, sp_os_get_path_kind()); } sp_str_t sp_fs_join_path(sp_mem_t mem, sp_str_t a, sp_str_t b) { - a = sp_fs_trim_path(a); - b = sp_fs_trim_path(b); - if (sp_str_empty(a)) return sp_str_copy(mem, b); - if (sp_str_empty(b)) return sp_str_copy(mem, a); - if (sp_str_back(a) == '/' || sp_str_back(a) == '\\') { - return sp_str_concat(mem, a, b); + return sp_fs_join_path_for(mem, a, b, sp_os_get_path_kind()); +} + +sp_str_t sp_fs_normalize_path(sp_mem_t mem, sp_str_t path) { + return sp_fs_normalize_path_for(mem, path, sp_os_get_path_kind()); +} + +sp_sys_fd_t sp_fs_get_cwd() { + return sp_sys_get_root(0); +} + +sp_path_t sp_path(sp_sys_fd_t dir, sp_str_t sub) { + return (sp_path_t) { .dir = dir, .sub = sub }; +} + +sp_path_t sp_path_cwd(sp_str_t sub) { + return sp_path(sp_fs_get_cwd(), sub); +} + +SP_PRIVATE s32 sp_fs_match_label(sp_str_t path, sp_str_t label) { + if (sp_str_empty(label)) return 0; + if (!sp_str_starts_with(path, label)) return SP_STR_NO_MATCH; + + u32 at = label.len; + bool boundary = at == path.len || sp_fs_is_sep(path.data[at]) || sp_fs_is_sep(sp_str_back(label)); + if (!boundary) return SP_STR_NO_MATCH; + + while (at < path.len && sp_fs_is_sep(path.data[at])) at++; + return (s32)at; +} + +sp_path_t sp_path_from_str(sp_str_t path) { + if (!sp_fs_is_absolute(path)) return sp_path_cwd(path); + + sp_path_t best = sp_path(SP_SYS_INVALID_FD, path); + s32 deepest = SP_STR_NO_MATCH; + for (s32 it = 0;; it++) { + sp_sys_fd_t root = sp_sys_get_root(it); + if (root == SP_SYS_INVALID_FD) return best; + + c8 buf [SP_PATH_MAX]; + u64 len = 0; + if (sp_sys_get_root_label(it, buf, SP_PATH_MAX, &len)) continue; + + s32 matched = sp_fs_match_label(path, sp_str(buf, (u32)len)); + if (matched <= deepest) continue; + + sp_str_t rest = sp_str_suffix(path, (s32)path.len - matched); + deepest = matched; + best = sp_path(root, sp_str_empty(rest) ? sp_str_lit(".") : rest); } - return sp_str_join(mem, a, b, sp_str_lit("/")); } -sp_str_t sp_fs_replace_ext(sp_mem_t mem, sp_str_t path, sp_str_t ext) { - sp_str_t stripped = sp_str_strip_right(path, sp_fs_get_ext(path)); - if (sp_str_back(stripped) == '.') stripped.len--; - return sp_str_empty(ext) ? - sp_str_copy(mem, stripped) : - sp_str_join(mem, stripped, ext, sp_str_lit(".")); +sp_path_t sp_path_join(sp_mem_t mem, sp_path_t path, sp_str_t sub) { + return sp_path(path.dir, sp_fs_join_path(mem, path.sub, sub)); } -sp_fs_kind_t sp_fs_get_link_kind(sp_str_t path) { - if (sp_str_empty(path)) return SP_FS_KIND_NONE; +sp_path_t sp_path_parent(sp_mem_t mem, sp_path_t path) { + if (sp_fs_is_dot(sp_fs_get_name(path.sub))) return sp_path_join(mem, path, sp_str_lit("..")); + return sp_path(path.dir, sp_fs_parent_path(path.sub)); +} + +sp_err_t sp_fs_open_dir_at(sp_path_t path, sp_sys_fd_t* out) { + return sp_sys_open_dir_s(path.dir, path.sub, 0, out); +} + +sp_fs_kind_t sp_fs_get_link_kind_at(sp_path_t path) { + if (sp_str_empty(path.sub)) return SP_FS_KIND_NONE; sp_sys_file_meta_t st = sp_zero; - s32 rc = sp_sys_get_link_metadata_s(sp_sys_get_root(0), path, &st); + s32 rc = sp_sys_get_link_metadata_s(path.dir, path.sub, &st); return rc == 0 ? st.kind : SP_FS_KIND_NONE; } -sp_fs_kind_t sp_fs_get_target_kind(sp_str_t path) { - if (sp_str_empty(path)) return SP_FS_KIND_NONE; +sp_fs_kind_t sp_fs_get_link_kind(sp_str_t path) { + return sp_fs_get_link_kind_at(sp_path_cwd(path)); +} + +sp_fs_kind_t sp_fs_get_target_kind_at(sp_path_t path) { + if (sp_str_empty(path.sub)) return SP_FS_KIND_NONE; sp_sys_file_meta_t st = sp_zero; - s32 rc = sp_sys_get_path_metadata_s(sp_sys_get_root(0), path, &st); + s32 rc = sp_sys_get_path_metadata_s(path.dir, path.sub, &st); return rc == 0 ? st.kind : SP_FS_KIND_NONE; } +sp_fs_kind_t sp_fs_get_target_kind(sp_str_t path) { + return sp_fs_get_target_kind_at(sp_path_cwd(path)); +} + +bool sp_fs_exists_at(sp_path_t path) { + return sp_fs_get_target_kind_at(path) != SP_FS_KIND_NONE; +} + bool sp_fs_exists(sp_str_t path) { - return sp_fs_get_target_kind(path) != SP_FS_KIND_NONE; + return sp_fs_exists_at(sp_path_cwd(path)); +} + +bool sp_fs_is_file_at(sp_path_t path) { + return sp_fs_get_link_kind_at(path) == SP_FS_KIND_FILE; } bool sp_fs_is_file(sp_str_t path) { - return sp_fs_get_link_kind(path) == SP_FS_KIND_FILE; + return sp_fs_is_file_at(sp_path_cwd(path)); +} + +bool sp_fs_is_symlink_at(sp_path_t path) { + return sp_fs_get_link_kind_at(path) == SP_FS_KIND_SYMLINK; } bool sp_fs_is_symlink(sp_str_t path) { - return sp_fs_get_link_kind(path) == SP_FS_KIND_SYMLINK; + return sp_fs_is_symlink_at(sp_path_cwd(path)); +} + +bool sp_fs_is_dir_at(sp_path_t path) { + return sp_fs_get_link_kind_at(path) == SP_FS_KIND_DIR; } bool sp_fs_is_dir(sp_str_t path) { - return sp_fs_get_link_kind(path) == SP_FS_KIND_DIR; + return sp_fs_is_dir_at(sp_path_cwd(path)); +} + +bool sp_fs_is_target_file_at(sp_path_t path) { + return sp_fs_get_target_kind_at(path) == SP_FS_KIND_FILE; } bool sp_fs_is_target_file(sp_str_t path) { - return sp_fs_get_target_kind(path) == SP_FS_KIND_FILE; + return sp_fs_is_target_file_at(sp_path_cwd(path)); +} + +bool sp_fs_is_target_dir_at(sp_path_t path) { + return sp_fs_get_target_kind_at(path) == SP_FS_KIND_DIR; } bool sp_fs_is_target_dir(sp_str_t path) { - return sp_fs_get_target_kind(path) == SP_FS_KIND_DIR; + return sp_fs_is_target_dir_at(sp_path_cwd(path)); +} + +sp_fs_kind_t sp_fs_get_kind_at(sp_path_t path) { + return sp_fs_get_link_kind_at(path); } sp_fs_kind_t sp_fs_get_kind(sp_str_t path) { - return sp_fs_get_link_kind(path); + return sp_fs_get_kind_at(sp_path_cwd(path)); } -sp_tm_epoch_t sp_fs_get_mod_time(sp_str_t path) { +sp_tm_epoch_t sp_fs_get_mod_time_at(sp_path_t path) { sp_tm_epoch_t result = sp_zero_s(sp_tm_epoch_t); sp_sys_file_meta_t st; - if (sp_sys_get_path_metadata_s(sp_sys_get_root(0), path, &st) == 0) { + if (sp_sys_get_path_metadata_s(path.dir, path.sub, &st) == 0) { result.s = (u64)st.mtime.tv_sec; result.ns = (u32)st.mtime.tv_nsec; } return result; } -sp_str_t sp_fs_canonicalize_path(sp_mem_t mem, sp_str_t path) { - if (sp_str_empty(path)) return sp_zero_s(sp_str_t); +sp_tm_epoch_t sp_fs_get_mod_time(sp_str_t path) { + return sp_fs_get_mod_time_at(sp_path_cwd(path)); +} - c8 buf[SP_PATH_MAX]; - s64 len = sp_sys_canonicalize_path_s(path, buf, SP_PATH_MAX); - if (len <= 0) return sp_zero_s(sp_str_t); +sp_err_t sp_fs_canonicalize_path_at(sp_mem_t mem, sp_path_t path, sp_str_t* canonical) { + sp_sys_fd_t fd = SP_SYS_INVALID_FD; + sp_try(sp_sys_open_s(path.dir, path.sub, SP_SYS_OPEN_MODE_PATH, 0, &fd)); + + c8 buf [SP_PATH_MAX]; + u64 len = 0; + sp_err_t err = sp_sys_get_fd_path(fd, buf, SP_PATH_MAX, &len); + sp_sys_close(fd); + sp_try(err); - return sp_fs_normalize_path(mem, sp_str(buf, len)); + *canonical = sp_str_copy(mem, sp_str(buf, (u32)len)); + return SP_OK; } -sp_str_t sp_fs_get_exe_path(sp_mem_t mem) { - c8 buf[SP_PATH_MAX]; - s64 len = sp_sys_get_exe_path(buf, SP_PATH_MAX); - if (len <= 0) return sp_str_lit(""); - return sp_fs_normalize_path(mem, sp_str(buf, len)); +sp_err_t sp_fs_canonicalize_path(sp_mem_t mem, sp_str_t path, sp_str_t* canonical) { + return sp_fs_canonicalize_path_at(mem, sp_path_cwd(path), canonical); } -sp_str_t sp_fs_get_cwd(sp_mem_t mem) { +sp_err_t sp_fs_get_exe_path(sp_mem_t mem, sp_str_t* path) { c8 buf[SP_PATH_MAX]; - s64 len = sp_sys_get_cwd_path(buf, SP_PATH_MAX); - if (len <= 0) return sp_zero_s(sp_str_t); - return sp_fs_normalize_path(mem, sp_str(buf, len)); + u64 len = 0; + sp_try(sp_sys_get_exe_path(buf, SP_PATH_MAX, &len)); + *path = sp_str_copy(mem, sp_str(buf, (u32)len)); + return SP_OK; } -sp_str_t sp_fs_resolve(sp_mem_t mem, sp_sys_fd_t fd) { -#if defined(SP_WIN32) - u16 wbuf[SP_PATH_MAX]; - DWORD wlen = GetFinalPathNameByHandleW((HANDLE)fd, (LPWSTR)wbuf, SP_PATH_MAX, 0); - if (wlen == 0 || wlen >= SP_PATH_MAX) return sp_zero_s(sp_str_t); - u32 skip = 0; - if (wlen >= 4 && wbuf[0] == '\\' && wbuf[1] == '\\' && wbuf[2] == '?' && wbuf[3] == '\\') skip = 4; - sp_str_t resolved; - if (sp_wtf16_to_wtf8(mem, (sp_wide_str_t) { .data = wbuf + skip, .len = wlen - skip }, &resolved) != SP_OK) return sp_zero_s(sp_str_t); - return resolved; - -#elif defined(SP_LINUX) - c8 self [64] = sp_zero; - sp_fmt_buf(self, 64, "/proc/self/fd/{}", sp_fmt_int(fd)); - - c8 buf [SP_PATH_MAX + 1] = sp_zero; - s64 n = sp_syscall(SP_SYSCALL_NUM_READLINKAT, SP_AT_FDCWD, self, buf, SP_PATH_MAX); - if (n <= 0) return sp_zero_s(sp_str_t); - return sp_str_from_cstr_n(mem, buf, n); - -#elif defined(SP_MACOS) || defined(SP_COSMO) - c8 buf[SP_PATH_MAX] = sp_zero; - if (fcntl((int)fd, F_GETPATH, buf) < 0) return sp_zero_s(sp_str_t); - return sp_str_from_cstr(mem, buf); - -#elif defined(SP_WASM) - (void)mem; (void)fd; - return sp_zero_s(sp_str_t); - -#else - #error "sp_fs_resolve" -#endif +sp_err_t sp_fs_get_cwd_path(sp_mem_t mem, sp_str_t* path) { + c8 buf[SP_PATH_MAX]; + u64 len = 0; + sp_try(sp_sys_get_fd_path(sp_fs_get_cwd(), buf, SP_PATH_MAX, &len)); + *path = sp_str_copy(mem, sp_str(buf, (u32)len)); + return SP_OK; } -sp_str_t sp_fs_get_storage_path(sp_mem_t mem) { +sp_err_t sp_fs_get_storage_path(sp_mem_t mem, sp_str_t* path) { c8 buf[SP_PATH_MAX]; - s64 len = sp_sys_get_storage_path(buf, SP_PATH_MAX); - if (len <= 0) return sp_zero_s(sp_str_t); - return sp_fs_normalize_path(mem, sp_str(buf, len)); + u64 len = 0; + sp_try(sp_sys_get_storage_path(buf, SP_PATH_MAX, &len)); + *path = sp_str_copy(mem, sp_str(buf, (u32)len)); + return SP_OK; } -sp_str_t sp_fs_get_config_path(sp_mem_t mem) { +sp_err_t sp_fs_get_config_path(sp_mem_t mem, sp_str_t* path) { c8 buf[SP_PATH_MAX]; - s64 len = sp_sys_get_config_path(buf, SP_PATH_MAX); - if (len <= 0) return sp_zero_s(sp_str_t); - return sp_fs_normalize_path(mem, sp_str(buf, len)); + u64 len = 0; + sp_try(sp_sys_get_config_path(buf, SP_PATH_MAX, &len)); + *path = sp_str_copy(mem, sp_str(buf, (u32)len)); + return SP_OK; } -sp_err_t sp_io_read_file(sp_mem_t mem, sp_str_t path, sp_str_t* content) { +sp_err_t sp_io_read_file_at(sp_mem_t mem, sp_path_t path, sp_str_t* content) { sp_assert(content); sp_err_t err = SP_OK; sp_err_t read_err = SP_OK; @@ -19973,7 +20478,7 @@ sp_err_t sp_io_read_file(sp_mem_t mem, sp_str_t path, sp_str_t* content) { u64 bytes_read = 0; sp_io_file_reader_t reader = sp_zero; - sp_try(sp_io_file_reader_from_path(&reader, path)); + sp_try(sp_io_file_reader_from_path_at(&reader, path)); sp_try_goto(sp_io_file_reader_size(&reader, &size), err, cleanup); if (!size) goto cleanup; @@ -19997,7 +20502,9 @@ sp_err_t sp_io_read_file(sp_mem_t mem, sp_str_t path, sp_str_t* content) { return err; } -SP_PRIVATE sp_err_t sp_fs_create_parent_at(sp_sys_fd_t dir, sp_str_t path); +sp_err_t sp_io_read_file(sp_mem_t mem, sp_str_t path, sp_str_t* content) { + return sp_io_read_file_at(mem, sp_path_cwd(path), content); +} SP_PRIVATE sp_err_t sp_fs_mkdir_at(sp_sys_fd_t dir, sp_str_t path) { sp_err_t err = sp_sys_mkdir_s(dir, path, sp_sys_default_dir_perms); @@ -20008,68 +20515,87 @@ SP_PRIVATE sp_err_t sp_fs_mkdir_at(sp_sys_fd_t dir, sp_str_t path) { return st.kind == SP_FS_KIND_DIR ? SP_OK : err; } -SP_PRIVATE sp_err_t sp_fs_create_dir_at(sp_sys_fd_t dir, sp_str_t path) { - sp_err_t err = sp_fs_mkdir_at(dir, path); +sp_err_t sp_fs_create_dir_at(sp_path_t path) { + sp_err_t err = sp_fs_mkdir_at(path.dir, path.sub); if (err != SP_ERR_SYS_NOT_FOUND) return err; - sp_try(sp_fs_create_parent_at(dir, path)); - return sp_fs_mkdir_at(dir, path); + sp_try(sp_fs_create_parent_at(path)); + return sp_fs_mkdir_at(path.dir, path.sub); } sp_err_t sp_fs_create_dir(sp_str_t path) { - return sp_fs_create_dir_at(sp_sys_get_root(0), sp_fs_trim_path(path)); + return sp_fs_create_dir_at(sp_path_cwd(path)); } -sp_err_t sp_fs_create_file(sp_str_t path) { +sp_err_t sp_fs_create_file_at(sp_path_t path) { sp_sys_fd_t fd = SP_SYS_INVALID_FD; - sp_try(sp_sys_open_s(sp_sys_get_root(0), path, SP_SYS_OPEN_MODE_WO, SP_SYS_OPEN_CREATE | SP_SYS_OPEN_TRUNCATE, &fd)); + sp_try(sp_sys_open_s(path.dir, path.sub, SP_SYS_OPEN_MODE_WO, SP_SYS_OPEN_CREATE | SP_SYS_OPEN_TRUNCATE, &fd)); return sp_sys_close(fd); } -sp_err_t sp_fs_create_file_slice(sp_str_t path, sp_mem_slice_t slice) { +sp_err_t sp_fs_create_file(sp_str_t path) { + return sp_fs_create_file_at(sp_path_cwd(path)); +} + +sp_err_t sp_fs_create_file_slice_at(sp_path_t path, sp_mem_slice_t slice) { sp_io_file_writer_t io = sp_zero; - sp_try(sp_io_file_writer_from_path(&io, path)); + sp_try(sp_io_file_writer_from_path_at(&io, path)); sp_err_t written = sp_io_write_all(&io.base, slice.data, slice.len, SP_NULLPTR); sp_err_t closed = sp_io_file_writer_close(&io); return written ? written : closed; } +sp_err_t sp_fs_create_file_slice(sp_str_t path, sp_mem_slice_t slice) { + return sp_fs_create_file_slice_at(sp_path_cwd(path), slice); +} + +sp_err_t sp_fs_create_file_str_at(sp_path_t path, sp_str_t str) { + return sp_fs_create_file_slice_at(path, sp_mem_slice((u8*)str.data, str.len)); +} + sp_err_t sp_fs_create_file_str(sp_str_t path, sp_str_t str) { - return sp_fs_create_file_slice(path, sp_mem_slice((u8*)str.data, str.len)); + return sp_fs_create_file_str_at(sp_path_cwd(path), str); +} + +sp_err_t sp_fs_create_file_cstr_at(sp_path_t path, const c8* str) { + return sp_fs_create_file_str_at(path, sp_str_view(str)); } sp_err_t sp_fs_create_file_cstr(sp_str_t path, const c8* str) { - return sp_fs_create_file_str(path, sp_str_view(str)); + return sp_fs_create_file_cstr_at(sp_path_cwd(path), str); +} + +sp_err_t sp_fs_create_hard_link_at(sp_path_t target, sp_path_t link_path) { + return sp_sys_link_s(target.dir, target.sub, link_path.dir, link_path.sub); } sp_err_t sp_fs_create_hard_link(sp_str_t target, sp_str_t link_path) { - return sp_sys_link_s(sp_sys_get_root(0), target, sp_sys_get_root(0), link_path); + return sp_fs_create_hard_link_at(sp_path_cwd(target), sp_path_cwd(link_path)); } -sp_err_t sp_fs_create_sym_link(sp_str_t target, sp_str_t link_path) { - return sp_sys_symlink_s(target, sp_sys_get_root(0), link_path); +sp_err_t sp_fs_create_sym_link_at(sp_str_t target, sp_path_t link_path, sp_fs_kind_t kind) { + return sp_sys_symlink_s(target, link_path.dir, link_path.sub, kind); } -sp_err_t sp_fs_link(sp_str_t from, sp_str_t to, sp_fs_link_kind_t kind) { - switch (kind) { - case SP_FS_LINK_HARD: return sp_fs_create_hard_link(from, to); - case SP_FS_LINK_SYMBOLIC: return sp_fs_create_sym_link(from, to); - } - SP_UNREACHABLE_RETURN(SP_OK); +sp_err_t sp_fs_create_sym_link(sp_str_t target, sp_str_t link_path, sp_fs_kind_t kind) { + return sp_fs_create_sym_link_at(target, sp_path_cwd(link_path), kind); +} + +sp_err_t sp_fs_remove_file_at(sp_path_t path) { + return sp_sys_unlink_s(path.dir, path.sub); } sp_err_t sp_fs_remove_file(sp_str_t path) { - return sp_sys_unlink_s(sp_sys_get_root(0), path); + return sp_fs_remove_file_at(sp_path_cwd(path)); } -sp_err_t sp_fs_dir_open(sp_fs_dir_t* it, sp_sys_fd_t fd, sp_str_t path, sp_mem_slice_t buf) { - *it = sp_zero_s(sp_fs_dir_t); - sp_assert(buf.len >= SP_SYS_DIR_MIN_BUF); +sp_err_t sp_fs_dir_it_open(sp_fs_dir_it_t* it, sp_path_t path, u32 flags, sp_mem_slice_t buf) { + *it = sp_zero_s(sp_fs_dir_it_t); sp_sys_fd_t dir_fd = SP_SYS_INVALID_FD; - sp_try(sp_sys_open_dir_s(fd, path, &dir_fd)); + sp_try(sp_sys_open_dir_s(path.dir, path.sub, flags, &dir_fd)); - sp_err_t err = sp_sys_dir_from_fd(dir_fd, &it->dir); + sp_err_t err = sp_sys_dir_it_open(dir_fd, &it->dir); if (err) { sp_sys_close(dir_fd); return err; @@ -20080,22 +20606,22 @@ sp_err_t sp_fs_dir_open(sp_fs_dir_t* it, sp_sys_fd_t fd, sp_str_t path, sp_mem_s return SP_OK; } -sp_err_t sp_fs_dir_next(sp_fs_dir_t* it, sp_fs_dir_entry_t* out) { +sp_err_t sp_fs_dir_it_next(sp_fs_dir_it_t* it, sp_fs_dir_entry_t* out) { *out = sp_zero_s(sp_fs_dir_entry_t); while (true) { // Pull another chunk from the kernel and advance the cursor if (it->cursor >= it->buf.len) { - sp_try(sp_sys_dir_read(&it->dir, &it->buf)); + sp_try(sp_sys_dir_it_read(&it->dir, &it->buf)); if (!it->buf.len) return SP_OK; it->cursor = 0; } sp_sys_dir_entry_t entry = sp_zero; - sp_try(sp_sys_dir_parse(&it->dir, &it->buf, &it->cursor, &entry)); + sp_try(sp_sys_dir_it_parse(&it->dir, &it->buf, &it->cursor, &entry)); sp_str_t name = sp_str(entry.name, entry.len); - if (sp_str_equal(name, sp_str_lit(".")) || sp_str_equal(name, sp_str_lit(".."))) continue; + if (sp_fs_is_dot(name)) continue; out->name = name; out->kind = entry.kind; @@ -20103,185 +20629,318 @@ sp_err_t sp_fs_dir_next(sp_fs_dir_t* it, sp_fs_dir_entry_t* out) { } } -sp_err_t sp_fs_dir_close(sp_fs_dir_t* it) { - return sp_sys_dir_close(&it->dir); +sp_err_t sp_fs_dir_it_close(sp_fs_dir_it_t* it) { + return sp_sys_dir_it_close(&it->dir); } SP_PRIVATE void sp_fs_it_unwind(sp_fs_it_t* it) { sp_da_for(it->stack, i) { - sp_fs_dir_close(&it->stack[i].dir); + sp_fs_dir_it_close(&it->stack[i].dir); } sp_da_clear(it->stack); } +SP_PRIVATE u32 sp_fs_it_child(sp_fs_it_t* it, u32 prefix) { + return prefix + (prefix && !sp_fs_is_sep(it->path[prefix - 1])); +} + SP_PRIVATE sp_err_t sp_fs_it_join(sp_fs_it_t* it, u32 prefix, sp_str_t name, u32* len) { - u32 sep = prefix && !sp_fs_is_sep(it->path[prefix - 1]); - if (prefix + sep + name.len >= SP_PATH_MAX) return SP_ERR_SYS_NAME_TOO_LONG; - if (sep) it->path[prefix] = '/'; - sp_str_copy_to(name, it->path + prefix + sep, name.len); - *len = prefix + sep + name.len; + u32 start = sp_fs_it_child(it, prefix); + if (start + name.len >= SP_PATH_MAX) return SP_ERR_SYS_NAME_TOO_LONG; + if (start > prefix) it->path[prefix] = '/'; + sp_str_copy_to(name, it->path + start, name.len); + *len = start + name.len; return SP_OK; } -SP_PRIVATE sp_err_t sp_fs_it_push(sp_fs_it_t* it, u32 len) { - sp_fs_it_frame_t frame = { .len = len }; - sp_try(sp_fs_dir_open(&frame.dir, sp_sys_get_root(0), sp_str(it->path, len), sp_mem_slice(frame.buf, SP_FS_IT_BUF_SIZE))); +SP_PRIVATE sp_err_t sp_fs_it_push(sp_fs_it_t* it, sp_path_t path, u32 len, u32 flags) { + sp_fs_it_frame_t frame = { .len = len, .name_len = path.sub.len }; + sp_try(sp_fs_dir_it_open(&frame.dir, path, flags, sp_mem_slice(frame.buf, SP_FS_IT_BUF_SIZE))); sp_da_push(it->stack, frame); return SP_OK; } -SP_PRIVATE sp_err_t sp_fs_it_open(sp_fs_it_t* it, sp_str_t path) { - u32 len = 0; - sp_try(sp_fs_it_join(it, 0, sp_fs_trim_path(path), &len)); - return sp_fs_it_push(it, len); -} - SP_PRIVATE sp_err_t sp_fs_it_step(sp_fs_it_t* it) { - while (!sp_da_empty(it->stack)) { - sp_fs_it_frame_t* top = sp_da_back(it->stack); - top->dir.buf.data = top->buf; - - sp_fs_dir_entry_t d = sp_zero; - sp_try(sp_fs_dir_next(&top->dir, &d)); - if (!d.name.data) { - sp_da_pop(it->stack); - sp_try(sp_fs_dir_close(&top->dir)); - continue; - } + sp_fs_it_frame_t* top = sp_da_back(it->stack); + top->dir.buf.data = top->buf; + u32 root = sp_fs_it_child(it, it->stack[0].len); + + sp_fs_dir_entry_t d = sp_zero; + sp_try(sp_fs_dir_it_next(&top->dir, &d)); + if (d.name.data) { u32 len = 0; sp_try(sp_fs_it_join(it, top->len, d.name, &len)); + it->yield = SP_FS_IT_ENTRY; it->entry = (sp_fs_entry_t) { .path = sp_str(it->path, len), + .rel = sp_str(it->path + root, len - root), .name = sp_str(it->path + len - d.name.len, d.name.len), .kind = d.kind, }; + it->at = sp_path(top->dir.dir.fd, it->entry.name); // If the OS didn't give us a hint, specifically get it from metadata if (it->entry.kind == SP_FS_KIND_NONE) { - it->entry.kind = sp_fs_get_link_kind(it->entry.path); - } - - if (it->recursive && it->entry.kind == SP_FS_KIND_DIR) { - sp_try(sp_fs_it_push(it, len)); + it->entry.kind = sp_fs_get_link_kind_at(it->at); } return SP_OK; } + + sp_da_pop(it->stack); + sp_try(sp_fs_dir_it_close(&top->dir)); + if (sp_da_empty(it->stack)) return SP_OK; + + sp_fs_it_frame_t* parent = sp_da_back(it->stack); + it->yield = SP_FS_IT_LEAVE; + it->entry = (sp_fs_entry_t) { + .path = sp_str(it->path, top->len), + .rel = sp_str(it->path + root, top->len - root), + .name = sp_str(it->path + top->len - top->name_len, top->name_len), + .kind = SP_FS_KIND_DIR, + }; + it->at = sp_path(parent->dir.dir.fd, it->entry.name); return SP_OK; } bool sp_fs_it_next(sp_fs_it_t* it) { - if (it->err) return false; + if (it->err || sp_da_empty(it->stack)) return false; it->entry = sp_zero_s(sp_fs_entry_t); + it->at = sp_zero_s(sp_path_t); it->err = sp_fs_it_step(it); if (it->err) sp_fs_it_unwind(it); return !it->err && !sp_da_empty(it->stack); } +sp_err_t sp_fs_it_enter(sp_fs_it_t* it) { + sp_assert(!sp_str_empty(it->entry.path)); + sp_assert(sp_da_back(it->stack)->len < it->entry.path.len); + return sp_fs_it_push(it, it->at, it->entry.path.len, SP_SYS_OPEN_DIR_NO_FOLLOW); +} + +bool sp_fs_it_walk(sp_fs_it_t* it) { + while (sp_fs_it_next(it)) { + if (it->yield == SP_FS_IT_LEAVE) continue; + if (it->entry.kind != SP_FS_KIND_DIR) return true; + + it->err = sp_fs_it_enter(it); + if (it->err) sp_fs_it_unwind(it); + return !it->err; + } + return false; +} + void sp_fs_it_deinit(sp_fs_it_t* it) { sp_fs_it_unwind(it); sp_da_free(it->stack); } +sp_fs_it_t sp_fs_it_new_at(sp_mem_t mem, sp_path_t path, u32 flags) { + sp_fs_it_t it = sp_zero; + sp_da_init(mem, it.stack); + + path.sub = sp_fs_trim_path(path.sub); + u32 len = 0; + it.err = sp_fs_it_join(&it, 0, path.sub, &len); + if (!it.err) it.err = sp_fs_it_push(&it, path, len, flags); + return it; +} + +sp_fs_it_t sp_fs_it_new(sp_mem_t mem, sp_str_t path) { + return sp_fs_it_new_at(mem, sp_path_cwd(path), 0); +} + SP_PRIVATE sp_fs_entry_t sp_fs_entry_copy(sp_mem_t mem, sp_fs_entry_t entry) { sp_str_t path = sp_str_copy(mem, entry.path); return (sp_fs_entry_t) { .path = path, + .rel = sp_str_suffix(path, (s32)entry.rel.len), .name = sp_str_suffix(path, (s32)entry.name.len), .kind = entry.kind, }; } -SP_PRIVATE sp_err_t sp_fs_collect_it(sp_mem_t mem, sp_fs_it_t* it, sp_da(sp_fs_entry_t)* out) { +sp_err_t sp_fs_collect(sp_mem_t mem, sp_str_t path, sp_da(sp_fs_entry_t)* out) { *out = sp_da_new(mem, sp_fs_entry_t); - while (sp_fs_it_next(it)) { - sp_da_push(*out, sp_fs_entry_copy(mem, it->entry)); + sp_fs_it_t it = sp_fs_it_new(mem, path); + while (sp_fs_it_next(&it)) { + sp_da_push(*out, sp_fs_entry_copy(mem, it.entry)); } - sp_fs_it_deinit(it); - return it->err; + sp_fs_it_deinit(&it); + return it.err; } -sp_err_t sp_fs_collect(sp_mem_t mem, sp_str_t path, sp_da(sp_fs_entry_t)* out) { +sp_err_t sp_fs_collect_recursive(sp_mem_t mem, sp_str_t path, sp_da(sp_fs_entry_t)* out) { + *out = sp_da_new(mem, sp_fs_entry_t); sp_fs_it_t it = sp_fs_it_new(mem, path); - return sp_fs_collect_it(mem, &it, out); + while (sp_fs_it_walk(&it)) { + sp_da_push(*out, sp_fs_entry_copy(mem, it.entry)); + } + sp_fs_it_deinit(&it); + return it.err; } -sp_err_t sp_fs_collect_recursive(sp_mem_t mem, sp_str_t path, sp_da(sp_fs_entry_t)* out) { - sp_fs_it_t it = sp_fs_it_new_recursive(mem, path); - return sp_fs_collect_it(mem, &it, out); +sp_err_t sp_fs_remove_dir(sp_str_t path) { + return sp_fs_remove_dir_at(sp_path_cwd(path)); } -sp_err_t sp_fs_remove_dir(sp_str_t path) { - sp_sys_file_meta_t st = sp_zero; - sp_try(sp_sys_get_link_metadata_s(sp_sys_get_root(0), path, &st)); +/* + sp_fs_remove_dir_at(path) - switch (st.kind) { - case SP_FS_KIND_DIR: { - break; - } - case SP_FS_KIND_SYMLINK: { - // remove the link itself, never the target; windows directory - // symlinks can only be removed as directories, so flip on refusal - sp_err_t err = sp_fs_remove_file(path); - if (err == SP_ERR_SYS_IS_DIR) err = sp_sys_rmdir_s(sp_sys_get_root(0), path); - return err; - } - default: { - return SP_ERR_SYS_NOT_DIR; - } - } + Iterate the entries produced by a path and fd, depth-first and traversing + into subdirectories, and unlink them. Fails if this root (i.e. fd + path) + does not refer to a directory, and unlinks the root at the end. + + This function uses O(depth) memory. A directory is reopened only when its + rmdir reports NOT_EMPTY after a pass that removed one of its entries. Every + path under the root must fit in SP_PATH_MAX; a deeper tree fails with + NAME_TOO_LONG, and can be removed by opening a handle partway down. + + + # SYMLINKS + +  When the path is a symlink, this function unlinks rather than resolving the + link and removing the contents in the link target. POSIX semantics say that + a trailing slash on the path follows the link implicitly. This is silly, we + don't encode arguments implicitly in strings. + + + # ERRORS + + ## NOT_FOUND + + When you ask to remove D, but D doesn't exist, we return NOT_FOUND. Often, + callers just want to *ensure* a directory's absence, and they don't care if + their call removed it, or someone else, or if it never existed. By returning + NOT_FOUND, we also let callers who DO care handle the case when they + expected something to exist but it didn't. + + When you ask to remove D, we may encounter NOT_FOUND while removing something + in D's tree. We do not report such errors; these aren't paths that the caller + named. For example, iterating D might yield D/M/W. The caller never specified + D/M/W. They asked us to remove everything under D, which is satisfied. + + The combination of these two give a nice property: NOT_FOUND is returned only + if nothing was removed. - sp_err_t err = SP_OK; - sp_mem_arena_marker_t s = sp_mem_begin_scratch(); - sp_da(sp_fs_entry_t) entries; - err = sp_fs_collect(s.mem, path, &entries); - if (err) goto done; + ## FAIL FAST - sp_da_for(entries, i) { - sp_fs_entry_t* entry = &entries[i]; + If removing D/M/W gives us some other error (e.g. it's read only), we + immediately fail. It's coherent to keep going, trying every entry that you + can, and sometimes what you want when cleaning up. But it's full of subtle + hidden rules, and it's much simpler for caller and library if we just tell + you that something failed and let you decide. - // entry kinds are hints; flip on a contradicting error so a lying or - // absent d_type can't strand an entry - if (entry->kind == SP_FS_KIND_DIR) { - err = sp_fs_remove_dir(entry->path); - if (err == SP_ERR_SYS_NOT_DIR) err = sp_fs_remove_file(entry->path); + + ## FINAL REMOVE CAN FAIL + + Once + + */ +SP_PRIVATE sp_err_t sp_fs_remove_unlisted(sp_path_t path) { + sp_err_t err = sp_sys_rmdir_s(path.dir, path.sub); + return err == SP_ERR_SYS_NOT_EMPTY ? SP_ERR_SYS_ACCESS_DENIED : err; +} + +SP_PRIVATE sp_err_t sp_fs_remove_enter(sp_fs_it_t* it) { + sp_err_t err = sp_fs_it_enter(it); + return err == SP_ERR_SYS_ACCESS_DENIED ? sp_fs_remove_unlisted(it->at) : err; +} + +SP_PRIVATE sp_err_t sp_fs_remove_pass(sp_path_t path, bool* progressed, bool* listed) { + sp_mem_arena_marker_t s = sp_mem_begin_scratch(); + sp_fs_it_t it = sp_fs_it_new_at(s.mem, path, SP_SYS_OPEN_DIR_NO_FOLLOW); + bool progress [SP_FS_IT_MAX_DEPTH] = sp_zero; + sp_err_t err = it.err; + *listed = err != SP_ERR_SYS_ACCESS_DENIED; + + while (!err && sp_fs_it_next(&it)) { + u64 depth = sp_da_size(it.stack); + switch (it.yield) { + case SP_FS_IT_LEAVE: { + err = sp_sys_rmdir_s(it.at.dir, it.at.sub); + if (err == SP_ERR_SYS_NOT_EMPTY && progress[depth]) err = sp_fs_it_enter(&it); + break; + } + case SP_FS_IT_ENTRY: { + // entry kinds are hints; flip on a contradicting error so a lying or + // absent d_type can't strand an entry + if (it.entry.kind == SP_FS_KIND_DIR) { + err = sp_fs_remove_enter(&it); + if (err == SP_ERR_SYS_NOT_DIR || err == SP_ERR_SYS_LOOP) err = sp_sys_unlink_s(it.at.dir, it.at.sub); + } + else { + err = sp_sys_unlink_s(it.at.dir, it.at.sub); + if (err == SP_ERR_SYS_IS_DIR) err = sp_fs_remove_enter(&it); + } + break; + } } - else { - err = sp_fs_remove_file(entry->path); - if (err == SP_ERR_SYS_IS_DIR) err = sp_fs_remove_dir(entry->path); + if (!err) { + if (sp_da_size(it.stack) > depth) progress[depth] = false; + else progress[depth - 1] = true; } - if (err) goto done; + if (err == SP_ERR_SYS_NOT_FOUND) err = SP_OK; } + if (!err) err = it.err; - err = sp_sys_rmdir_s(sp_sys_get_root(0), path); - -done: + *progressed = progress[0]; + sp_fs_it_deinit(&it); sp_mem_end_scratch(s); return err; } +sp_err_t sp_fs_remove_dir_at(sp_path_t path) { + sp_str_t name = sp_fs_get_name(path.sub); + if (sp_str_empty(name) || sp_fs_is_dot(name)) return SP_ERR_SYS_INVALID; + + sp_sys_file_meta_t meta = sp_zero; + sp_try(sp_sys_get_link_metadata_s(path.dir, path.sub, &meta)); + switch (meta.kind) { + case SP_FS_KIND_DIR: break; + case SP_FS_KIND_SYMLINK: return sp_sys_unlink_s(path.dir, path.sub); + case SP_FS_KIND_FILE: + case SP_FS_KIND_NONE: return SP_ERR_SYS_NOT_DIR; + } + + bool progressed = false; + while (true) { + bool retry = progressed; + bool listed = false; + sp_err_t err = sp_fs_remove_pass(path, &progressed, &listed); + if (err == SP_ERR_SYS_NOT_FOUND && retry) return SP_OK; + if (err && listed) return err; + err = listed ? sp_sys_rmdir_s(path.dir, path.sub) : sp_fs_remove_unlisted(path); + if (err == SP_ERR_SYS_NOT_EMPTY && progressed) continue; + return err == SP_ERR_SYS_NOT_FOUND ? SP_OK : err; + } +} + static sp_atomic_s32_t sp_fs_atomic_sequence; -SP_PRIVATE sp_str_t sp_fs_temp_name(sp_str_t dir, sp_str_t name, c8 buf [SP_PATH_MAX]) { +SP_PRIVATE sp_err_t sp_fs_temp_name(sp_str_t dir, sp_str_t name, c8 buf [SP_PATH_MAX], sp_str_t* temp) { sp_tm_epoch_t now = sp_tm_now_epoch(); u64 stamp = (now.s << 20) ^ (u64)now.ns; u64 sequence = (u64)(u32)sp_atomic_s32_add(&sp_fs_atomic_sequence, 1, SP_ATOMIC_SEQ_CST); + c8 leaf_buf [SP_PATH_MAX]; sp_io_mem_writer_t io = sp_zero; - sp_io_mem_writer_from_buffer(&io, buf, SP_PATH_MAX); - if (sp_str_empty(dir)) { - sp_fmt_io(&io.base, ".{}.{}.{}.tmp", sp_fmt_str(name), sp_fmt_uint(stamp), sp_fmt_uint(sequence)); - } else { - sp_fmt_io(&io.base, "{}/.{}.{}.{}.tmp", sp_fmt_str(dir), sp_fmt_str(name), sp_fmt_uint(stamp), sp_fmt_uint(sequence)); - } - return sp_io_mem_writer_as_str(&io); + sp_io_mem_writer_from_buffer(&io, leaf_buf, SP_PATH_MAX); + sp_fmt_io(&io.base, ".{}.{}.{}.tmp", sp_fmt_str(name), sp_fmt_uint(stamp), sp_fmt_uint(sequence)); + + sp_str_t leaf = sp_io_mem_writer_as_str(&io); + if (dir.len + leaf.len + 1 >= SP_PATH_MAX) return SP_ERR_SYS_NAME_TOO_LONG; + + sp_mem_fixed_t fixed = sp_mem_fixed_ex(buf, SP_PATH_MAX, 1); + *temp = sp_fs_join_path(sp_mem_fixed_as_allocator(&fixed), dir, leaf); + return SP_OK; } -SP_PRIVATE sp_err_t sp_fs_create_parent_at(sp_sys_fd_t dir, sp_str_t path) { - sp_str_t parent = sp_fs_parent_path(path); - if (sp_str_empty(parent)) return SP_OK; - sp_err_t err = sp_fs_create_dir_at(dir, parent); +SP_API sp_err_t sp_fs_create_parent_at(sp_path_t path) { + sp_str_t parent = sp_fs_parent_path(path.sub); + if (sp_str_equal(parent, sp_fs_trim_path(path.sub))) return SP_OK; + sp_err_t err = sp_fs_create_dir_at(sp_path(path.dir, parent)); return err == SP_ERR_SYS_EXISTS ? SP_ERR_SYS_NOT_DIR : err; } @@ -20289,15 +20948,11 @@ SP_PRIVATE sp_err_t sp_fs_atomic_create(sp_fs_atomic_t* af, sp_sys_fd_t dir, sp_ *af = sp_zero_s(sp_fs_atomic_t); af->dir = dir; af->path = path; - af->temp = sp_fs_temp_name(temp_dir, sp_fs_get_name(path), af->temp_buf); u32 flags = SP_SYS_OPEN_CREATE | SP_SYS_OPEN_EXCLUSIVE; sp_sys_fd_t fd = SP_SYS_INVALID_FD; - sp_err_t err = sp_sys_open_s(dir, af->temp, SP_SYS_OPEN_MODE_WO, flags, &fd); - if (err == SP_ERR_SYS_NOT_FOUND) { - err = sp_fs_create_parent_at(dir, af->temp); - if (!err) err = sp_sys_open_s(dir, af->temp, SP_SYS_OPEN_MODE_WO, flags, &fd); - } + sp_err_t err = sp_fs_temp_name(temp_dir, sp_fs_get_name(path), af->temp_buf, &af->temp); + if (!err) err = sp_sys_open_s(dir, af->temp, SP_SYS_OPEN_MODE_WO, flags, &fd); if (err) { *af = sp_zero_s(sp_fs_atomic_t); return err; @@ -20311,17 +20966,21 @@ SP_PRIVATE sp_err_t sp_fs_atomic_create(sp_fs_atomic_t* af, sp_sys_fd_t dir, sp_ return err; } -sp_err_t sp_fs_atomic_open_at(sp_fs_atomic_t* af, sp_sys_fd_t dir, sp_str_t path) { - return sp_fs_atomic_create(af, dir, path, sp_fs_parent_path(path)); +sp_err_t sp_fs_atomic_open_at(sp_fs_atomic_t* af, sp_path_t path) { + return sp_fs_atomic_create(af, path.dir, path.sub, sp_fs_parent_path(path.sub)); } sp_err_t sp_fs_atomic_open(sp_fs_atomic_t* af, sp_str_t path) { - return sp_fs_atomic_create(af, sp_sys_get_root(0), path, sp_fs_parent_path(path)); + return sp_fs_atomic_open_at(af, sp_path_cwd(path)); +} + +sp_err_t sp_fs_atomic_open_staged_at(sp_fs_atomic_t* af, sp_path_t path, sp_str_t staging) { + sp_assert(!sp_str_empty(staging)); + return sp_fs_atomic_create(af, path.dir, path.sub, staging); } sp_err_t sp_fs_atomic_open_staged(sp_fs_atomic_t* af, sp_str_t path, sp_str_t staging) { - if (sp_str_empty(staging)) return SP_ERR_SYS_BUG; - return sp_fs_atomic_create(af, sp_sys_get_root(0), path, staging); + return sp_fs_atomic_open_staged_at(af, sp_path_cwd(path), staging); } sp_io_writer_t* sp_fs_atomic_writer(sp_fs_atomic_t* af) { @@ -20348,14 +21007,7 @@ sp_err_t sp_fs_atomic_commit(sp_fs_atomic_t* af, sp_fs_atomic_mode_t mode) { sp_err_t close_err = sp_io_file_writer_close(&af->writer); if (!err) err = close_err; - if (!err) { - err = sp_fs_atomic_publish(af, mode); - if (err == SP_ERR_SYS_NOT_FOUND) { - err = sp_fs_create_parent_at(af->dir, af->path); - if (!err) err = sp_fs_atomic_publish(af, mode); - } - } - + if (!err) err = sp_fs_atomic_publish(af, mode); if (err) sp_sys_unlink_s(af->dir, af->temp); af->temp = sp_zero_s(sp_str_t); return err; @@ -20378,198 +21030,144 @@ SP_PRIVATE sp_err_t sp_fs_atomic_write(sp_fs_atomic_t* af, const void* data, u64 return sp_fs_atomic_commit(af, SP_FS_ATOMIC_REPLACE); } -sp_err_t sp_fs_write_atomic_slice_staged(sp_str_t path, sp_str_t staging, sp_mem_slice_t slice) { +sp_err_t sp_fs_write_atomic_slice_staged_at(sp_path_t path, sp_str_t staging, sp_mem_slice_t slice) { sp_fs_atomic_t af = sp_zero; - sp_try(sp_fs_atomic_open_staged(&af, path, staging)); + sp_try(sp_fs_atomic_open_staged_at(&af, path, staging)); return sp_fs_atomic_write(&af, slice.data, slice.len); } -sp_err_t sp_fs_write_atomic_slice(sp_str_t path, sp_mem_slice_t slice) { - sp_fs_atomic_t af = sp_zero; - sp_try(sp_fs_atomic_open(&af, path)); - return sp_fs_atomic_write(&af, slice.data, slice.len); +sp_err_t sp_fs_write_atomic_slice_staged(sp_str_t path, sp_str_t staging, sp_mem_slice_t slice) { + return sp_fs_write_atomic_slice_staged_at(sp_path_cwd(path), staging, slice); } -sp_err_t sp_fs_write_atomic(sp_str_t path, sp_str_t str) { +sp_err_t sp_fs_write_atomic_slice_at(sp_path_t path, sp_mem_slice_t slice) { sp_fs_atomic_t af = sp_zero; - sp_try(sp_fs_atomic_open(&af, path)); - return sp_fs_atomic_write(&af, str.data, str.len); + sp_try(sp_fs_atomic_open_at(&af, path)); + return sp_fs_atomic_write(&af, slice.data, slice.len); } -sp_err_t sp_fs_write_atomic_cstr(sp_str_t path, const c8* str) { - return sp_fs_write_atomic(path, sp_str_view(str)); +sp_err_t sp_fs_write_atomic_slice(sp_str_t path, sp_mem_slice_t slice) { + return sp_fs_write_atomic_slice_at(sp_path_cwd(path), slice); } -SP_PRIVATE sp_err_t sp_fs_copy_bytes(sp_str_t from, sp_str_t to, sp_fs_atomic_mode_t mode, sp_sys_file_perms_t perms) { - sp_io_file_reader_t reader = sp_zero; - sp_try(sp_io_file_reader_from_path(&reader, from)); - +sp_err_t sp_fs_write_atomic_at(sp_path_t path, sp_str_t str) { sp_fs_atomic_t af = sp_zero; - sp_err_t err = sp_fs_atomic_create(&af, sp_sys_get_root(0), to, sp_fs_parent_path(to)); - if (err) { - sp_io_file_reader_close(&reader); - return err; - } + sp_try(sp_fs_atomic_open_at(&af, path)); + return sp_fs_atomic_write(&af, str.data, str.len); +} - err = sp_io_copy(sp_fs_atomic_writer(&af), &reader.base, SP_NULLPTR); - sp_io_file_reader_close(&reader); - if (!err) err = sp_sys_set_file_perms_s(af.dir, af.temp, perms); - if (err) { - sp_fs_atomic_abort(&af); - return err; - } - return sp_fs_atomic_commit(&af, mode); +sp_err_t sp_fs_write_atomic(sp_str_t path, sp_str_t str) { + return sp_fs_write_atomic_at(sp_path_cwd(path), str); } -SP_PRIVATE sp_err_t sp_fs_copy_tree_file(sp_str_t from, sp_str_t to, sp_fs_atomic_mode_t mode) { - sp_sys_file_meta_t meta = sp_zero; - sp_try(sp_sys_get_path_metadata_s(sp_sys_get_root(0), from, &meta)); - return sp_fs_copy_bytes(from, to, mode, meta.perms); +sp_err_t sp_fs_write_atomic_cstr_at(sp_path_t path, const c8* str) { + return sp_fs_write_atomic_at(path, sp_str_view(str)); } -SP_PRIVATE sp_err_t sp_fs_copy_link(sp_str_t from, sp_str_t to, sp_fs_atomic_mode_t mode) { - sp_sys_fd_t root = sp_sys_get_root(0); +sp_err_t sp_fs_write_atomic_cstr(sp_str_t path, const c8* str) { + return sp_fs_write_atomic_cstr_at(sp_path_cwd(path), str); +} +SP_PRIVATE sp_err_t sp_fs_copy_link_at(sp_path_t from, sp_path_t to, sp_fs_atomic_mode_t mode) { c8 buf [SP_PATH_MAX]; sp_str_t target = sp_zero; - sp_try(sp_sys_readlink_s(root, from, buf, sizeof(buf), &target)); + sp_try(sp_sys_readlink_s(from.dir, from.sub, buf, sizeof(buf), &target)); + sp_fs_kind_t kind = sp_fs_get_target_kind_at(from) == SP_FS_KIND_DIR ? SP_FS_KIND_DIR : SP_FS_KIND_FILE; switch (mode) { case SP_FS_ATOMIC_REPLACE: { - c8 temp_buf [SP_PATH_MAX]; - sp_str_t temp = sp_fs_temp_name(sp_fs_parent_path(to), sp_fs_get_name(to), temp_buf); - sp_try(sp_sys_symlink_s(target, root, temp)); - sp_err_t err = sp_sys_rename_s(root, temp, root, to); - if (err) sp_sys_unlink_s(root, temp); - return err; + sp_err_t err = sp_sys_unlink_s(to.dir, to.sub); + if (err && err != SP_ERR_SYS_NOT_FOUND) return err; + return sp_sys_symlink_s(target, to.dir, to.sub, kind); } case SP_FS_ATOMIC_EXCLUSIVE: { - return sp_sys_symlink_s(target, root, to); + return sp_sys_symlink_s(target, to.dir, to.sub, kind); } } SP_UNREACHABLE_RETURN(SP_OK); } -sp_err_t sp_fs_copy_file(sp_str_t from, sp_str_t to, sp_fs_atomic_mode_t mode) { +sp_err_t sp_fs_copy_file_at(sp_path_t from, sp_path_t to, sp_fs_atomic_mode_t mode) { + sp_io_file_reader_t reader = sp_zero; + sp_try(sp_io_file_reader_from_path_at(&reader, from)); + sp_sys_file_meta_t meta = sp_zero; - sp_try(sp_sys_get_path_metadata_s(sp_sys_get_root(0), from, &meta)); + sp_err_t err = sp_sys_get_file_metadata(reader.file, &meta); + if (!err) { + switch (meta.kind) { + case SP_FS_KIND_FILE: break; + case SP_FS_KIND_DIR: err = SP_ERR_SYS_IS_DIR; break; + case SP_FS_KIND_SYMLINK: + case SP_FS_KIND_NONE: err = SP_ERR_SYS_UNSUPPORTED; break; + } + } + if (err) { + sp_io_file_reader_close(&reader); + return err; + } - switch (meta.kind) { - case SP_FS_KIND_FILE: return sp_fs_copy_bytes(from, to, mode, meta.perms); - case SP_FS_KIND_DIR: return SP_ERR_SYS_IS_DIR; - case SP_FS_KIND_SYMLINK: - case SP_FS_KIND_NONE: return SP_ERR_SYS_UNSUPPORTED; + sp_fs_atomic_t af = sp_zero; + err = sp_fs_atomic_open_at(&af, to); + if (err) { + sp_io_file_reader_close(&reader); + return err; } - SP_UNREACHABLE_RETURN(SP_OK); -} -SP_PRIVATE bool sp_fs_path_within(sp_str_t path, sp_str_t root) { - return sp_str_starts_with(path, root) && (path.len == root.len || sp_fs_is_sep(path.data[root.len])); -} + err = sp_io_copy(sp_fs_atomic_writer(&af), &reader.base, SP_NULLPTR); + sp_io_file_reader_close(&reader); -SP_PRIVATE bool sp_fs_tree_contains(sp_mem_t mem, sp_str_t from, sp_str_t to) { - sp_str_t root = sp_fs_canonicalize_path(mem, from); - sp_str_t nearest = sp_zero; - while (sp_str_empty(nearest) && !sp_str_empty(to) && !sp_fs_is_root(to)) { - nearest = sp_fs_canonicalize_path(mem, to); - to = sp_fs_parent_path(to); + // @spader I'm not sure if eating this error is right. My gut says that it + // is not. On WASM, the copy could work, but you simply can't set perms. + if (!err) { + err = sp_sys_set_file_perms_s(af.dir, af.temp, meta.perms); + if (err == SP_ERR_SYS_UNSUPPORTED) err = SP_OK; } - return sp_fs_path_within(nearest, root); + if (err) { + sp_fs_atomic_abort(&af); + return err; + } + return sp_fs_atomic_commit(&af, mode); } -sp_err_t sp_fs_copy_tree(sp_str_t from, sp_str_t to, sp_fs_atomic_mode_t mode) { - from = sp_fs_trim_path(from); - to = sp_fs_trim_path(to); - u32 skip = from.len + !sp_fs_is_sep(sp_str_back(from)); - +sp_err_t sp_fs_copy_tree_at(sp_path_t from, sp_path_t to, sp_fs_atomic_mode_t mode) { sp_mem_arena_marker_t s = sp_mem_begin_scratch(); - sp_fs_it_t it = sp_fs_it_new_recursive(s.mem, from); + sp_fs_it_t it = sp_fs_it_new_at(s.mem, from, 0); + sp_sys_fd_t fd = SP_SYS_INVALID_FD; sp_err_t err = it.err; - if (!err && sp_fs_tree_contains(s.mem, from, to)) err = SP_ERR_SYS_INVALID; - if (!err) err = sp_fs_create_dir(to); + if (!err) err = sp_fs_create_dir_at(to); + if (!err) err = sp_fs_open_dir_at(to, &fd); - while (!err && sp_fs_it_next(&it)) { - sp_str_t rel = sp_str_sub(it.entry.path, (s32)skip, (s32)(it.entry.path.len - skip)); - sp_str_t dst = sp_fs_join_path(s.mem, to, rel); + while (!err && sp_fs_it_walk(&it)) { + sp_path_t dst = sp_path(fd, it.entry.rel); switch (it.entry.kind) { - case SP_FS_KIND_DIR: err = sp_fs_create_dir(dst); break; - case SP_FS_KIND_FILE: err = sp_fs_copy_tree_file(it.entry.path, dst, mode); break; - case SP_FS_KIND_SYMLINK: err = sp_fs_copy_link(it.entry.path, dst, mode); break; + case SP_FS_KIND_DIR: err = sp_fs_create_dir_at(dst); break; + case SP_FS_KIND_FILE: err = sp_fs_copy_file_at(it.at, dst, mode); break; + case SP_FS_KIND_SYMLINK: err = sp_fs_copy_link_at(it.at, dst, mode); break; case SP_FS_KIND_NONE: err = SP_ERR_SYS_UNSUPPORTED; break; } } if (!err) err = it.err; sp_fs_it_deinit(&it); + if (fd != SP_SYS_INVALID_FD) sp_sys_close(fd); sp_mem_end_scratch(s); return err; } -bool sp_fs_glob_match(sp_str_t name, sp_str_t glob) { - u32 n = 0; - u32 g = 0; - u32 star_n = 0; - u32 star_g = 0; - bool starred = false; - - while (n < name.len) { - if (g < glob.len && glob.data[g] == '*') { - starred = true; - star_g = ++g; - star_n = n; - } - else if (g < glob.len && glob.data[g] == name.data[n]) { - g++; - n++; - } - else if (starred) { - g = star_g; - n = ++star_n; - } - else { - return false; - } - } - - while (g < glob.len && glob.data[g] == '*') g++; - return g == glob.len; -} - -sp_err_t sp_fs_copy(sp_str_t from, sp_str_t to) { +sp_err_t sp_fs_copy_at(sp_path_t from, sp_path_t to, sp_fs_atomic_mode_t mode) { sp_sys_file_meta_t meta = sp_zero; - sp_try(sp_sys_get_path_metadata_s(sp_sys_get_root(0), from, &meta)); + sp_try(sp_sys_get_path_metadata_s(from.dir, from.sub, &meta)); switch (meta.kind) { - case SP_FS_KIND_FILE: return sp_fs_copy_bytes(from, to, SP_FS_ATOMIC_REPLACE, meta.perms); - case SP_FS_KIND_DIR: return sp_fs_copy_tree(from, to, SP_FS_ATOMIC_REPLACE); + case SP_FS_KIND_FILE: return sp_fs_copy_file_at(from, to, mode); + case SP_FS_KIND_DIR: return sp_fs_copy_tree_at(from, to, mode); case SP_FS_KIND_SYMLINK: case SP_FS_KIND_NONE: return SP_ERR_SYS_UNSUPPORTED; } SP_UNREACHABLE_RETURN(SP_OK); } -sp_err_t sp_fs_copy_into(sp_str_t from, sp_str_t dir) { - sp_mem_arena_marker_t s = sp_mem_begin_scratch(); - sp_err_t err = sp_fs_copy(from, sp_fs_join_path(s.mem, dir, sp_fs_get_name(sp_fs_trim_path(from)))); - sp_mem_end_scratch(s); - return err; -} - -sp_fs_it_t sp_fs_it_new(sp_mem_t mem, sp_str_t path) { - sp_fs_it_t it = { .mem = mem }; - sp_da_init(mem, it.stack); - it.err = sp_fs_it_open(&it, path); - return it; -} - -sp_fs_it_t sp_fs_it_new_recursive(sp_mem_t mem, sp_str_t path) { - sp_fs_it_t it = { .mem = mem, .recursive = true }; - sp_da_init(mem, it.stack); - it.err = sp_fs_it_open(&it, path); - return it; -} - // // time // diff --git a/sp/sp_cli.h b/sp/sp_cli.h index 1396eb10..43d206bc 100644 --- a/sp/sp_cli.h +++ b/sp/sp_cli.h @@ -1381,14 +1381,12 @@ SP_PRIVATE sp_str_t sp_cli_completer_path(sp_cli_desc_t desc, c8* buffer, u32 le if (sp_fs_is_absolute(arg0)) return arg0; c8 cwd [SP_PATH_MAX]; - s64 cwd_len = sp_sys_get_cwd_path(cwd, sizeof(cwd)); - if (cwd_len <= 0) return arg0; + u64 cwd_len = 0; + if (sp_sys_get_fd_path(sp_fs_get_cwd(), cwd, sizeof(cwd), &cwd_len)) return arg0; if (sp_cast(u32, cwd_len) + 1 + arg0.len > len) return arg0; - sp_io_mem_writer_t path = sp_zero; - sp_io_mem_writer_from_buffer(&path, buffer, len); - sp_fmt_io(&path.base, "{}/{}", sp_fmt_str(sp_str(cwd, sp_cast(u32, cwd_len))), sp_fmt_str(arg0)); - return sp_io_mem_writer_as_str(&path); + sp_mem_fixed_t fixed = sp_mem_fixed_ex(buffer, len, 1); + return sp_fs_join_path(sp_mem_fixed_as_allocator(&fixed), sp_str(cwd, sp_cast(u32, cwd_len)), arg0); } void sp_cli_write_completions(sp_io_writer_t* io, sp_cli_desc_t desc, sp_cli_shell_t shell) { diff --git a/sp/sp_glob.h b/sp/sp_glob.h index b6a7deda..38d5497b 100644 --- a/sp/sp_glob.h +++ b/sp/sp_glob.h @@ -118,6 +118,7 @@ SP_IMP bool sp_glob_match_class(sp_glob_token_t* tok, c8 c); SP_IMP bool sp_glob_match_alternates(sp_glob_token_t* tok, sp_da(sp_glob_token_t) tokens, u32 t, sp_str_t path, u32 p); SP_IMP bool sp_glob_match_tokens(sp_da(sp_glob_token_t) tokens, sp_str_t path); SP_IMP sp_glob_strategy_t sp_glob_detect_strategy(sp_glob_t* glob); +SP_IMP sp_str_t sp_glob_basename(sp_str_t path); SP_IMP sp_glob_candidate_t sp_glob_candidate_new(sp_str_t path); SP_IMP void sp_glob_set_ht_add(sp_mem_t mem, sp_glob_set_index_table_t* ht, sp_str_t key, u32 idx); SP_IMP void sp_glob_set_ht_collect(sp_glob_set_index_table_t ht, sp_str_t key, sp_da(u32)* out); @@ -559,6 +560,11 @@ sp_glob_t* sp_glob_new_str(sp_mem_t mem, sp_str_t pattern) { return g; } +sp_str_t sp_glob_basename(sp_str_t path) { + s32 slash = sp_str_find_c8_reverse(path, '/'); + return slash == SP_STR_NO_MATCH ? path : sp_str_suffix(path, (s32)path.len - slash - 1); +} + bool sp_glob_match(sp_glob_t* g, sp_str_t path) { SP_ASSERT(g); @@ -567,7 +573,7 @@ bool sp_glob_match(sp_glob_t* g, sp_str_t path) { return sp_str_equal(g->literal, path); } case SP_GLOB_STRATEGY_BASENAME_LITERAL: { - sp_str_t basename = sp_fs_get_name(path); + sp_str_t basename = sp_glob_basename(path); return sp_str_equal(g->literal, basename); } case SP_GLOB_STRATEGY_EXTENSION: { @@ -596,10 +602,12 @@ bool sp_glob_match(sp_glob_t* g, sp_str_t path) { } sp_glob_candidate_t sp_glob_candidate_new(sp_str_t path) { + sp_str_t basename = sp_glob_basename(path); + s32 dot = sp_str_find_c8_reverse(basename, '.'); return (sp_glob_candidate_t){ .path = path, - .basename = sp_fs_get_name(path), - .ext = sp_fs_get_ext(path), + .basename = basename, + .ext = dot == SP_STR_NO_MATCH ? sp_zero_s(sp_str_t) : sp_str_suffix(basename, (s32)basename.len - dot - 1), }; } diff --git a/sp/sp_msvc.h b/sp/sp_msvc.h index 1b347c2c..499d87b1 100644 --- a/sp/sp_msvc.h +++ b/sp/sp_msvc.h @@ -452,14 +452,14 @@ static sp_msvc_err_t sp_msvc_find_sdks(sp_msvc_t* msvc, sp_msvc_arch_t arch) { sp_str_t lib = sp_fmt_buf(buf, sizeof(buf), "{}/Lib", sp_fmt_str(sp_msvc_path_str(&root))).value; SP_ALIGNED u8 dir_buf [SP_SYS_DIR_MIN_BUF]; - sp_fs_dir_t dir = sp_zero; - if (sp_fs_dir_open(&dir, sp_sys_get_root(0), lib, sp_mem_slice(dir_buf, sizeof(dir_buf)))) { + sp_fs_dir_it_t dir = sp_zero; + if (sp_fs_dir_it_open(&dir, sp_path_cwd(lib), 0, sp_mem_slice(dir_buf, sizeof(dir_buf)))) { return SP_MSVC_ERR_SDK_NOT_FOUND; } while (true) { sp_fs_dir_entry_t entry = sp_zero; - if (sp_fs_dir_next(&dir, &entry)) break; + if (sp_fs_dir_it_next(&dir, &entry)) break; if (!entry.name.data) break; if (entry.kind != SP_FS_KIND_DIR) continue; if (!sp_str_starts_with(entry.name, sp_str_lit("10."))) continue; @@ -470,7 +470,7 @@ static sp_msvc_err_t sp_msvc_find_sdks(sp_msvc_t* msvc, sp_msvc_arch_t arch) { sp_msvc_add_sdk(msvc, &sdk); } - sp_fs_dir_close(&dir); + sp_fs_dir_it_close(&dir); if (!msvc->num_sdks) return SP_MSVC_ERR_SDK_NOT_FOUND; return SP_MSVC_OK; @@ -499,14 +499,14 @@ static sp_msvc_err_t sp_msvc_find_installations(sp_msvc_t* msvc, sp_msvc_arch_t if (instances.err) return SP_MSVC_ERR_VS_NOT_FOUND; SP_ALIGNED u8 dir_buf [SP_SYS_DIR_MIN_BUF]; - sp_fs_dir_t dir = sp_zero; - if (sp_fs_dir_open(&dir, sp_sys_get_root(0), instances.value, sp_mem_slice(dir_buf, sizeof(dir_buf)))) { + sp_fs_dir_it_t dir = sp_zero; + if (sp_fs_dir_it_open(&dir, sp_path_cwd(instances.value), 0, sp_mem_slice(dir_buf, sizeof(dir_buf)))) { return SP_MSVC_ERR_VS_NOT_FOUND; } while (true) { sp_fs_dir_entry_t entry = sp_zero; - if (sp_fs_dir_next(&dir, &entry)) break; + if (sp_fs_dir_it_next(&dir, &entry)) break; if (!entry.name.data) break; if (entry.kind != SP_FS_KIND_DIR) continue; @@ -538,7 +538,7 @@ static sp_msvc_err_t sp_msvc_find_installations(sp_msvc_t* msvc, sp_msvc_arch_t sp_msvc_add_vs(msvc, &vs); } - sp_fs_dir_close(&dir); + sp_fs_dir_it_close(&dir); if (!msvc->num_installations) return SP_MSVC_ERR_VS_NOT_FOUND; return SP_MSVC_OK; diff --git a/sp/sp_test.h b/sp/sp_test.h index 3e9348f3..c8d82143 100644 --- a/sp/sp_test.h +++ b/sp/sp_test.h @@ -369,6 +369,14 @@ typedef struct { #define sp_test_skip_on_linux() #endif +#if defined(SP_WASM) + #define sp_test_skip_without_absolute_names() return sp_test_skip(t, "no absolute names on wasm"); +#else + #define sp_test_skip_without_absolute_names() ((void)0) +#endif + +#define sp_test_skip_without_symlinks() if (!sp_test_symlinks_supported(t)) return sp_test_skip(t, "symlinks not available"); + SP_API sp_err_t sp_test_skip(sp_test_t* t, const c8* fmt, ...); SP_API void sp_test_fail(sp_test_t* t, const c8* fmt, ...); SP_API sp_str_t sp_test_get_name(sp_test_t* t); @@ -386,7 +394,7 @@ SP_API void sp_test_note(sp_test_t* t, const c8* fmt, ...); SP_API sp_mem_t sp_test_mem(sp_test_t* t); SP_API sp_mem_t sp_test_arena(sp_test_t* t); -SP_API sp_str_t sp_test_dir(sp_test_t* t); +SP_API sp_path_t sp_test_dir(sp_test_t* t); SP_API const void* sp_test_user(sp_test_t* t); SP_API void sp_test_record(sp_test_t* t, sp_test_failure_t failure); @@ -397,8 +405,6 @@ SP_API bool sp_test_mem_eq(sp_test_t* t, const void* lhs, const void* rhs SP_API bool sp_test_strs_eq(sp_test_t* t, const sp_str_t* actual, u64 count, const c8* const* expect, const c8* sa, const c8* se, sp_str_t file, u32 line); SP_API void sp_test_golden(sp_test_t* t, sp_str_t path, sp_str_t actual, sp_str_t file, u32 line); -SP_API void sp_test_golden_abs(sp_test_t* t, sp_str_t path, sp_str_t actual, sp_str_t file, u32 line); -SP_API sp_da(sp_str_t) sp_test_resolve_roots(sp_mem_t mem, sp_str_t cwd, sp_str_t exe_dir); typedef struct { sp_atomic_s32_t state; @@ -408,6 +414,8 @@ typedef struct { SP_TYPEDEF_FN(sp_err_t, sp_test_once_fn_t, void* user); SP_API sp_err_t sp_test_once(sp_test_once_t* once, sp_test_once_fn_t fn, void* user); +SP_API bool sp_test_symlinks_supported(sp_test_t* t); + typedef enum { SP_TEST_ALLOC_LIVE, @@ -663,8 +671,7 @@ SP_API sp_str_t sp_test_value_opaque(sp_test_t* t, ...); #define sp_expect_err_eq(T, A, B) sp_test_err_eq(T, A, B, #A, #B, sp_test_soft) #define sp_must_err_eq(T, A, B) sp_test_err_eq(T, A, B, #A, #B, sp_test_stop) -#define sp_expect_golden(T, PATH, ACTUAL) sp_test_golden(T, PATH, ACTUAL, sp_cstr_as_str(__FILE__), (u32)__LINE__) -#define sp_expect_golden_abs(T, PATH, ACTUAL) sp_test_golden_abs(T, PATH, ACTUAL, sp_cstr_as_str(__FILE__), (u32)__LINE__) +#define sp_expect_golden(T, PATH, ACTUAL) sp_test_golden(T, PATH, ACTUAL, sp_cstr_as_str(__FILE__), (u32)__LINE__) #endif @@ -720,7 +727,7 @@ struct sp_test_t { sp_mem_heap_t* tracking_heap; sp_mem_t tracked_mem; - sp_str_t dir; + sp_path_t dir; }; struct sp_test_runner_t { @@ -733,8 +740,9 @@ struct sp_test_runner_t { sp_da(const c8*) failed; sp_da(const c8*) skipped; sp_da(const c8*) updated; - sp_str_t dir_root; - sp_str_t golden_root; + sp_path_t dir_root; + sp_test_once_t symlinks; + sp_path_t golden_root; bool update; sp_test_keep_t keep; bool kept; @@ -1013,11 +1021,22 @@ sp_mem_t sp_test_mem(sp_test_t* t) { return t->tracked_mem; } -sp_str_t sp_test_dir(sp_test_t* t) { - if (sp_str_empty(t->dir)) { +static sp_str_t sp_test_path_str(sp_mem_t mem, sp_path_t path) { + c8 buf [SP_PATH_MAX]; + u64 len = 0; + for (s32 it = 0; sp_sys_get_root(it) != SP_SYS_INVALID_FD; it++) { + if (sp_sys_get_root(it) != path.dir) continue; + if (sp_sys_get_root_label(it, buf, sizeof(buf), &len)) break; + return sp_fs_join_path(mem, sp_str(buf, (u32)len), path.sub); + } + return sp_str_copy(mem, path.sub); +} + +sp_path_t sp_test_dir(sp_test_t* t) { + if (sp_str_empty(t->dir.sub)) { sp_str_t leaf = sp_str_replace_c8(t->mem, sp_cstr_as_str(t->instance->name), '/', '_'); - t->dir = sp_fs_join_path(t->mem, t->runner->dir_root, leaf); - sp_err_t err = sp_fs_create_dir(t->dir); + t->dir = sp_path_join(t->mem, t->runner->dir_root, leaf); + sp_err_t err = sp_fs_create_dir_at(t->dir); if (err) sp_test_fail(t, "failed to create test dir: {}", sp_fmt_str(sp_test_err_str(t, err))); } return t->dir; @@ -1036,115 +1055,118 @@ sp_err_t sp_test_once(sp_test_once_t* once, sp_test_once_fn_t fn, void* user) { return once->err; } +static sp_err_t sp_test_probe_symlinks(void* user) { + sp_test_runner_t* runner = (sp_test_runner_t*)user; + sp_mem_arena_marker_t s = sp_mem_begin_scratch(); + sp_path_t link = sp_path_join(s.mem, runner->dir_root, sp_str_lit("L")); + sp_err_t err = sp_fs_create_dir_at(runner->dir_root); + if (!err) err = sp_fs_create_sym_link_at(sp_str_lit("A"), link, SP_FS_KIND_FILE); + if (!err) sp_fs_remove_file_at(link); + sp_mem_end_scratch(s); + return err; +} + +bool sp_test_symlinks_supported(sp_test_t* t) { + return !sp_test_once(&t->runner->symlinks, sp_test_probe_symlinks, t->runner); +} + //////////// // GOLDEN // //////////// -sp_da(sp_str_t) sp_test_resolve_roots(sp_mem_t mem, sp_str_t cwd, sp_str_t exe_dir) { - sp_da(sp_str_t) roots = sp_da_new(mem, sp_str_t); - - sp_str_t anchors [] = { cwd, exe_dir }; - sp_carr_for(anchors, at) { - if (sp_str_empty(anchors[at])) continue; - - sp_str_t dir = sp_fs_normalize_path(mem, anchors[at]); - while (!sp_str_empty(dir)) { - bool seen = false; - sp_da_for(roots, it) { - if (sp_str_equal(roots[it], dir)) { seen = true; break; } - } - if (!seen) sp_da_push(roots, dir); +static sp_path_t sp_test_golden_search(sp_mem_t mem, sp_path_t dir, sp_str_t file) { + while (true) { + if (sp_fs_is_target_file_at(sp_path_join(mem, dir, file))) return dir; - sp_str_t parent = sp_fs_parent_path(dir); - if (sp_str_equal(parent, dir)) break; - dir = parent; - } + sp_path_t parent = sp_path_parent(mem, dir); + sp_sys_file_meta_t from = sp_zero; + sp_sys_file_meta_t to = sp_zero; + if (sp_sys_get_path_metadata_s(dir.dir, dir.sub, &from)) return sp_zero_s(sp_path_t); + if (sp_sys_get_path_metadata_s(parent.dir, parent.sub, &to)) return sp_zero_s(sp_path_t); + if (from.id == to.id && from.device == to.device) return sp_zero_s(sp_path_t); + dir = parent; } - - return roots; } -static sp_str_t sp_test_golden_root(sp_test_t* t, sp_str_t file) { +static sp_path_t sp_test_golden_root(sp_test_t* t, sp_str_t file) { sp_test_runner_t* runner = t->runner; sp_mutex_lock(&runner->mutex); - sp_str_t root = runner->golden_root; + sp_path_t root = runner->golden_root; sp_mutex_unlock(&runner->mutex); - if (!sp_str_empty(root)) return root; + if (!sp_str_empty(root.sub)) return root; - sp_str_t cwd = sp_fs_get_cwd(t->mem); - sp_str_t exe_dir = sp_fs_parent_path(sp_fs_get_exe_path(t->mem)); - sp_da(sp_str_t) roots = sp_test_resolve_roots(t->mem, cwd, exe_dir); + sp_path_t anchors [2] = { sp_path_cwd(sp_str_lit(".")) }; + sp_str_t exe = sp_zero; + if (!sp_fs_get_exe_path(t->mem, &exe)) anchors[1] = sp_path_from_str(sp_fs_parent_path(exe)); - sp_da_for(roots, it) { - if (!sp_fs_is_target_file(sp_fs_join_path(t->mem, roots[it], file))) continue; - root = roots[it]; - break; + sp_carr_for(anchors, it) { + if (sp_str_empty(anchors[it].sub)) continue; + root = sp_test_golden_search(t->mem, anchors[it], file); + if (!sp_str_empty(root.sub)) break; } - if (sp_str_empty(root)) return root; + if (sp_str_empty(root.sub)) return root; sp_mutex_lock(&runner->mutex); - if (sp_str_empty(runner->golden_root)) { - runner->golden_root = sp_str_copy(runner->mem, root); + if (sp_str_empty(runner->golden_root.sub)) { + runner->golden_root = sp_path(root.dir, sp_str_copy(runner->mem, root.sub)); } root = runner->golden_root; sp_mutex_unlock(&runner->mutex); return root; } -static void sp_test_golden_at(sp_test_t* t, sp_str_t path, sp_str_t actual, sp_str_t file, u32 line) { - sp_str_t actual_path = sp_test_format(t, "{}.actual", sp_fmt_str(path)); +static void sp_test_golden_at(sp_test_t* t, sp_path_t path, sp_str_t actual, sp_str_t file, u32 line) { + sp_str_t shown = sp_test_path_str(t->mem, path); + sp_path_t actual_path = sp_path(path.dir, sp_test_format(t, "{}.actual", sp_fmt_str(path.sub))); if (t->runner->update) { - sp_str_t parent = sp_fs_parent_path(path); - if (!sp_str_empty(parent) && !sp_fs_exists(parent)) { - sp_fs_create_dir(parent); - } + sp_fs_create_dir_at(sp_path_parent(t->mem, path)); - sp_err_t err = sp_fs_create_file_str(path, actual); + sp_err_t err = sp_fs_create_file_str_at(path, actual); if (err) { sp_test_record(t, (sp_test_failure_t) { .file = file, .line = line, .message = sp_test_format(t, "failed to update golden {}: err {}", - sp_fmt_str(path), sp_fmt_int(err)), + sp_fmt_str(shown), sp_fmt_int(err)), }); return; } - if (sp_fs_exists(actual_path)) sp_fs_remove_file(actual_path); + if (sp_fs_exists_at(actual_path)) sp_fs_remove_file_at(actual_path); t->updated++; return; } - if (!sp_fs_exists(path)) { + if (!sp_fs_exists_at(path)) { sp_test_record(t, (sp_test_failure_t) { .file = file, .line = line, .message = sp_test_format(t, "golden {} does not exist; run with --update to create it", - sp_fmt_str(path)), + sp_fmt_str(shown)), }); return; } sp_str_t want = sp_zero; - sp_err_t err = sp_io_read_file(t->mem, path, &want); + sp_err_t err = sp_io_read_file_at(t->mem, path, &want); if (err) { sp_test_record(t, (sp_test_failure_t) { .file = file, .line = line, .message = sp_test_format(t, "failed to read golden {}: err {}", - sp_fmt_str(path), sp_fmt_int(err)), + sp_fmt_str(shown), sp_fmt_int(err)), }); return; } if (sp_str_equal(want, actual)) { - if (sp_fs_exists(actual_path)) sp_fs_remove_file(actual_path); + if (sp_fs_exists_at(actual_path)) sp_fs_remove_file_at(actual_path); return; } - sp_fs_create_file_str(actual_path, actual); + sp_fs_create_file_str_at(actual_path, actual); sp_da(sp_str_t) want_lines = sp_str_split_c8(t->mem, want, '\n'); sp_da(sp_str_t) got_lines = sp_str_split_c8(t->mem, actual, '\n'); @@ -1162,29 +1184,22 @@ static void sp_test_golden_at(sp_test_t* t, sp_str_t path, sp_str_t actual, sp_s .file = file, .line = line, .message = sp_test_format(t, "golden mismatch at {}:{}; wrote {}", - sp_fmt_str(path), sp_fmt_uint(diff + 1), sp_fmt_str(actual_path)), + sp_fmt_str(shown), sp_fmt_uint(diff + 1), sp_fmt_str(sp_test_path_str(t->mem, actual_path))), .expected = sp_test_format(t, "{.quote}", sp_fmt_str(want_line)), .actual = sp_test_format(t, "{.quote}", sp_fmt_str(got_line)), }); } void sp_test_golden(sp_test_t* t, sp_str_t path, sp_str_t actual, sp_str_t file, u32 line) { - if (sp_fs_is_absolute_for(path, SP_FS_PATH_WINDOWS)) { - sp_test_record(t, (sp_test_failure_t) { - .file = file, - .line = line, - .message = sp_test_format(t, "golden path {} is absolute; paths resolve against the calling source dir (use sp_expect_golden_abs)", - sp_fmt_str(path)), - }); - return; - } - file = sp_fs_normalize_path(t->mem, file); - sp_str_t src = file; - if (!sp_fs_is_absolute_for(file, SP_FS_PATH_WINDOWS)) { - sp_str_t root = sp_test_golden_root(t, file); - if (sp_str_empty(root)) { + sp_path_t src = sp_zero; + if (sp_fs_is_absolute_for(file, SP_FS_PATH_WINDOWS)) { + src = sp_path_from_str(file); + } + else { + sp_path_t root = sp_test_golden_root(t, file); + if (sp_str_empty(root.sub)) { sp_test_record(t, (sp_test_failure_t) { .file = file, .line = line, @@ -1193,24 +1208,23 @@ void sp_test_golden(sp_test_t* t, sp_str_t path, sp_str_t actual, sp_str_t file, }); return; } - src = sp_fs_join_path(t->mem, root, file); + src = sp_path_join(t->mem, root, file); } - if (!sp_fs_is_target_file(src)) { + if (src.dir == SP_SYS_INVALID_FD || !sp_fs_is_target_file_at(src)) { sp_test_record(t, (sp_test_failure_t) { .file = file, .line = line, .message = sp_test_format(t, "cannot locate {}; goldens resolve against the calling source dir (--golden-root overrides)", - sp_fmt_str(src)), + sp_fmt_str(sp_test_path_str(t->mem, src))), }); return; } - sp_test_golden_at(t, sp_fs_join_path(t->mem, sp_fs_parent_path(src), path), actual, file, line); -} - -void sp_test_golden_abs(sp_test_t* t, sp_str_t path, sp_str_t actual, sp_str_t file, u32 line) { - sp_test_golden_at(t, path, actual, file, line); + sp_path_t golden = sp_fs_is_absolute(path) ? + sp_path_from_str(path) : + sp_path_join(t->mem, sp_path_parent(t->mem, src), path); + sp_test_golden_at(t, golden, actual, file, line); } @@ -1400,8 +1414,8 @@ static sp_test_status_t sp_test_status(sp_test_t* t) { return SP_TEST_STATUS_OK; } -static sp_err_t sp_test_remove_dir(sp_str_t path) { - sp_err_t err = sp_fs_remove_dir(path); +static sp_err_t sp_test_remove_dir(sp_path_t path) { + sp_err_t err = sp_fs_remove_dir_at(path); return err == SP_ERR_SYS_NOT_FOUND ? SP_OK : err; } @@ -1444,8 +1458,8 @@ static void sp_test_report(sp_test_t* t, sp_tty_t* term, sp_test_status_t status sp_tty_fmt(term, " {.gray} {}\n", sp_fmt_cstr("note"), sp_fmt_str(t->notes[it])); } - if (!sp_str_empty(t->dir)) { - sp_tty_fmt(term, " {.gray} {}\n", sp_fmt_cstr("dir"), sp_fmt_str(t->dir)); + if (!sp_str_empty(t->dir.sub)) { + sp_tty_fmt(term, " {.gray} {}\n", sp_fmt_cstr("dir"), sp_fmt_str(sp_test_path_str(t->mem, t->dir))); } sp_da_for(t->failures, it) { @@ -1580,14 +1594,14 @@ static void sp_test_run_instance(sp_test_runner_t* runner, const sp_test_instanc case SP_TEST_KEEP_ALWAYS: keep = true; break; } - if (!sp_str_empty(t->dir) && !keep) { + if (!sp_str_empty(t->dir.sub) && !keep) { sp_err_t err = sp_test_remove_dir(t->dir); if (err) { sp_test_fail(t, "failed to remove test dir: {}", sp_fmt_str(sp_test_err_str(t, err))); status = sp_test_status(t); } else { - t->dir = sp_zero_s(sp_str_t); + t->dir = sp_zero_s(sp_path_t); } } @@ -1601,7 +1615,7 @@ static void sp_test_run_instance(sp_test_runner_t* runner, const sp_test_instanc sp_mutex_lock(&runner->mutex); sp_io_write_str(&runner->out.base, text, SP_NULLPTR); sp_io_flush(&runner->out.base); - if (!sp_str_empty(t->dir)) runner->kept = true; + if (!sp_str_empty(t->dir.sub)) runner->kept = true; switch (status) { case SP_TEST_STATUS_FAIL: sp_da_push(runner->failed, instance->name); break; case SP_TEST_STATUS_SKIP: sp_da_push(runner->skipped, instance->name); break; @@ -1892,15 +1906,6 @@ static sp_str_t sp_test_run_name(sp_mem_t mem, sp_tm_epoch_t time) { return sp_str_replace_c8(mem, sp_tm_epoch_to_iso8601(mem, time), ':', '-'); } -static s32 sp_test_run_sort_kernel(const void* a, const void* b) { - const sp_fs_entry_t* ea = (const sp_fs_entry_t*)a; - const sp_fs_entry_t* eb = (const sp_fs_entry_t*)b; - bool da = ea->kind == SP_FS_KIND_DIR; - bool db = eb->kind == SP_FS_KIND_DIR; - if (da != db) return da ? SP_QSORT_A_FIRST : SP_QSORT_B_FIRST; - return sp_str_compare_alphabetical(ea->name, eb->name); -} - s32 sp_test_main(s32 argc, const c8** argv, const sp_test_entry_t* entries) { const c8* filter = SP_NULLPTR; u32 jobs = 1; @@ -1958,7 +1963,7 @@ s32 sp_test_main(s32 argc, const c8** argv, const sp_test_entry_t* entries) { { .name = "dir", .kind = SP_CLI_OPT_CSTR, - .summary = "root for test directories (defaults to $cwd/.sp/test)", + .summary = "root for test directories (defaults to .sp/test)", .placeholder = "dir", .ptr = &dir.opt, }, @@ -2010,7 +2015,14 @@ s32 sp_test_main(s32 argc, const c8** argv, const sp_test_entry_t* entries) { sp_str_t golden_root = sp_zero; if (golden.env) golden_root = sp_cstr_as_str(golden.env); if (golden.opt) golden_root = sp_cstr_as_str(golden.opt); - if (!sp_str_empty(golden_root)) runner->golden_root = sp_fs_normalize_path(runner->mem, golden_root); + if (!sp_str_empty(golden_root)) { + runner->golden_root = sp_path_from_str(sp_fs_normalize_path(runner->mem, golden_root)); + if (runner->golden_root.dir == SP_SYS_INVALID_FD) { + sp_fmt_io(&runner->out.base, "no root serves {.quote}\n", sp_fmt_str(golden_root)); + sp_io_flush(&runner->out.base); + return 1; + } + } sp_mutex_init(&runner->mutex); sp_io_stream_writer_from_fd(&runner->out, sp_sys_stdout, SP_IO_CLOSE_MODE_NONE); @@ -2097,35 +2109,41 @@ s32 sp_test_main(s32 argc, const c8** argv, const sp_test_entry_t* entries) { return 0; } - sp_str_t root = sp_str_lit(".sp/test"); - if (dir.env) root = sp_cstr_as_str(dir.env); - if (dir.opt) root = sp_cstr_as_str(dir.opt); - root = sp_fs_normalize_path(runner->mem, root); - if (!sp_fs_is_absolute(root)) root = sp_fs_join_path(runner->mem, sp_fs_get_cwd(runner->mem), root); + sp_str_t dir_name = sp_str_lit(".sp/test"); + if (dir.env) dir_name = sp_cstr_as_str(dir.env); + if (dir.opt) dir_name = sp_cstr_as_str(dir.opt); + dir_name = sp_fs_normalize_path(runner->mem, dir_name); + + sp_path_t root = sp_path_from_str(dir_name); + if (root.dir == SP_SYS_INVALID_FD) { + sp_fmt_io(&runner->out.base, "no root serves {.quote}\n", sp_fmt_str(dir_name)); + sp_io_flush(&runner->out.base); + return 1; + } sp_tm_epoch_t now = sp_tm_now_epoch(); sp_tm_epoch_t grace = { .s = now.s - SP_TEST_RUNS_GRACE_S, .ns = now.ns }; - sp_str_t runs_dir = sp_fs_join_path(runner->mem, root, sp_fs_get_stem(sp_fs_get_exe_path(runner->mem))); - runner->dir_root = sp_fs_join_path(runner->mem, runs_dir, sp_test_run_name(runner->mem, now)); + sp_str_t exe = sp_zero; + if (sp_fs_get_exe_path(runner->mem, &exe)) exe = sp_cstr_as_str(argv[0]); + sp_path_t runs_dir = sp_path_join(runner->mem, root, sp_fs_get_stem(exe)); + runner->dir_root = sp_path_join(runner->mem, runs_dir, sp_test_run_name(runner->mem, now)); sp_str_t cutoff = sp_test_run_name(runner->mem, grace); - sp_da(sp_fs_entry_t) listing = SP_NULLPTR; - sp_fs_collect(runner->mem, runs_dir, &listing); - sp_da_sort(listing, sp_test_run_sort_kernel); - - u64 runs = 0; - sp_da_for(listing, it) { - if (listing[it].kind != SP_FS_KIND_DIR) break; - runs++; + sp_da(sp_str_t) runs = sp_da_new(runner->mem, sp_str_t); + sp_fs_for(runner->mem, runs_dir, it) { + if (it.entry.kind != SP_FS_KIND_DIR) continue; + sp_da_push(runs, sp_str_copy(runner->mem, it.entry.name)); } + sp_da_sort(runs, sp_str_sort_kernel_alphabetical); - u64 candidates = runs > SP_TEST_RUNS_RETAINED ? runs - SP_TEST_RUNS_RETAINED : 0; + u64 candidates = sp_da_size(runs) > SP_TEST_RUNS_RETAINED ? sp_da_size(runs) - SP_TEST_RUNS_RETAINED : 0; sp_for(it, candidates) { - if (sp_str_compare_alphabetical(listing[it].name, cutoff) != SP_QSORT_A_FIRST) break; - sp_err_t err = sp_test_remove_dir(listing[it].path); + if (sp_str_compare_alphabetical(runs[it], cutoff) != SP_QSORT_A_FIRST) break; + sp_path_t run = sp_path_join(runner->mem, runs_dir, runs[it]); + sp_err_t err = sp_test_remove_dir(run); if (err) { sp_tty_fmt(&runner->term, "> failed to prune {}: {}\n", - sp_fmt_str(listing[it].path), + sp_fmt_str(run.sub), sp_fmt_str(sp_test_err_name(runner->mem, err))); } } @@ -2191,7 +2209,7 @@ s32 sp_test_main(s32 argc, const c8** argv, const sp_test_entry_t* entries) { if (err) { sp_tty_fmt(&runner->term, "> {.red} {}: {}\n", sp_fmt_cstr("failed to remove"), - sp_fmt_str(runner->dir_root), + sp_fmt_str(runner->dir_root.sub), sp_fmt_str(sp_test_err_name(runner->mem, err))); sp_io_flush(&runner->out.base); } diff --git a/spn.toml b/spn.toml index c5d6094f..daa37b50 100644 --- a/spn.toml +++ b/spn.toml @@ -14,15 +14,15 @@ name = "amalgamation" source = [ "test/main.c", "test/test/axis.c", "test/test/filter.c", "test/test/matrix.c", - "test/test/resolve.c", "test/test/sweep.c", "test/test/tracking.c", - "test/sys/sys.c", "test/sys/open.c", "test/sys/path.c", "test/sys/iter.c", "test/sys/futex.c", - "test/sys/fd.c", "test/sys/nanosleep.c", "test/sys/timestamps.c", "test/sys/perms.c", "test/sys/self_path.c", + "test/test/sweep.c", "test/test/tracking.c", + "test/sys/sys.c", "test/sys/open.c", "test/sys/path.c", "test/sys/futex.c", + "test/sys/fd.c", "test/sys/fd_path.c", "test/sys/nanosleep.c", "test/sys/timestamps.c", "test/sys/perms.c", "test/sys/self_path.c", "test/sys/pipe/data.c", "test/sys/pipe/blocking.c", "test/sys/transfer.c", "test/sys/readlink.c", "test/sys/socket/data.c", "test/sys/socket/listen.c", "test/sys/socket/desc.c", - "test/sys/tty/contract.c", "test/sys/tty/mode.c", "test/sys/vtable.c", + "test/sys/tty/contract.c", "test/sys/tty/mode.c", "test/sys/wait/event.c", "test/sys/wait/wait.c", "test/sys/inherit.c", - "test/fs/dir/*.c", "test/fs/it/*.c", "test/fs/wtf8/*.c", + "test/fs/dir/*.c", "test/fs/it/*.c", "test/fs/remove/*.c", "test/fs/wtf8/*.c", "test/fs/*.c", "test/fs/windows/nt_path.c", "test/msvc/*.c", "test/mem/uninitialized.c", @@ -39,11 +39,11 @@ source = ["test/test/*.c"] name = "sys" source = [ "test/main.c", - "test/sys/sys.c", "test/sys/open.c", "test/sys/path.c", "test/sys/iter.c", "test/sys/futex.c", - "test/sys/fd.c", "test/sys/nanosleep.c", "test/sys/timestamps.c", "test/sys/perms.c", "test/sys/self_path.c", + "test/sys/sys.c", "test/sys/open.c", "test/sys/path.c", "test/sys/futex.c", + "test/sys/fd.c", "test/sys/fd_path.c", "test/sys/nanosleep.c", "test/sys/timestamps.c", "test/sys/perms.c", "test/sys/self_path.c", "test/sys/pipe/data.c", "test/sys/pipe/blocking.c", "test/sys/transfer.c", "test/sys/readlink.c", "test/sys/socket/data.c", "test/sys/socket/listen.c", "test/sys/socket/desc.c", - "test/sys/tty/contract.c", "test/sys/tty/mode.c", "test/sys/vtable.c", + "test/sys/tty/contract.c", "test/sys/tty/mode.c", "test/sys/wait/event.c", "test/sys/wait/wait.c", "test/sys/inherit.c" ] @@ -54,7 +54,7 @@ source = ["test/main.c", "test/atomic.c"] [[test]] name = "fs" -source = ["test/main.c", "test/fs/dir/*.c", "test/fs/it/*.c", "test/fs/wtf8/*.c", "test/fs/*.c", "test/fs/windows/nt_path.c"] +source = ["test/main.c", "test/fs/dir/*.c", "test/fs/it/*.c", "test/fs/remove/*.c", "test/fs/wtf8/*.c", "test/fs/*.c", "test/fs/windows/nt_path.c"] include = [".", "test/fs"] [[test]] @@ -101,6 +101,10 @@ source = ["example/zero_copy.c"] name = "msvc" source = ["example/msvc.c"] +[[example]] +name = "iterator" +source = ["example/fs/iterator.c"] + [profile.sanitizer] linkage = "shared" sanitize = ["address", "undefined"] diff --git a/test/bench/ubench.h b/test/bench/ubench.h index da4083fa..88a13a77 100644 --- a/test/bench/ubench.h +++ b/test/bench/ubench.h @@ -1488,7 +1488,8 @@ s32 ubench_main(s32 argc, const c8 *const argv[]) { returns, which is what binds the strings into SQLite). */ { sp_mem_arena_marker_t s = sp_mem_begin_scratch(); - sp_str_t exe = sp_fs_get_exe_path(s.mem); + sp_str_t exe = sp_zero; + sp_fs_get_exe_path(s.mem, &exe); sp_cstr_copy_to_n(exe.data, exe.len, exe_path_buf, sizeof(exe_path_buf)); if (exe_path_buf[0]) { diff --git a/test/cli/complete.c b/test/cli/complete.c index 69ad501e..25491db1 100644 --- a/test/cli/complete.c +++ b/test/cli/complete.c @@ -409,9 +409,11 @@ static void run_cli_completer_path_test(s32* utest_result, sp_mem_t mem, cli_com if (t.expect.anchored) { c8 cwd [SP_PATH_MAX]; - s64 cwd_len = sp_sys_get_cwd_path(cwd, sizeof(cwd)); - EXPECT_GT(cwd_len, 0); - if (cwd_len <= 0) return; + u64 cwd_len = 0; + sp_err_t err = sp_sys_get_fd_path(sp_fs_get_cwd(), cwd, sizeof(cwd), &cwd_len); + if (err == SP_ERR_SYS_UNSUPPORTED) UTEST_SKIP("no cwd path"); + EXPECT_EQ(err, SP_OK); + if (err) return; sp_str_t expected = sp_fmt(mem, "{}/{}", sp_fmt_str(sp_str(cwd, sp_cast(u32, cwd_len))), sp_fmt_cstr(t.arg0)).value; SP_EXPECT_STR_EQ(path, expected); } @@ -434,12 +436,22 @@ UTEST_F(cli_complete, completer_path_relative) { } UTEST_F(cli_complete, completer_path_relative_backslash) { + SKIP_UNLESS_WIN32() + run_cli_completer_path_test(&ur, ut.mem.arena, (cli_completer_path_test_t) { .arg0 = "B\\A", .expect = { .anchored = true }, }); } +UTEST_F(cli_complete, completer_path_backslash_is_a_name) { + SKIP_ON_WIN32() + + run_cli_completer_path_test(&ur, ut.mem.arena, (cli_completer_path_test_t) { + .arg0 = "B\\A", + }); +} + UTEST_F(cli_complete, completer_path_absolute) { run_cli_completer_path_test(&ur, ut.mem.arena, (cli_completer_path_test_t) { .arg0 = "/B/A", diff --git a/test/cli/usage.c b/test/cli/usage.c index 1ff0f10c..8b8bbb5b 100644 --- a/test/cli/usage.c +++ b/test/cli/usage.c @@ -19,7 +19,9 @@ static sp_str_t cli_usage_golden_dir(sp_mem_t mem) { sp_str_t baked = sp_str_lit(SP_CLI_TEST_DIR); if (sp_fs_exists(baked)) return sp_fs_join_path(mem, baked, sp_str_lit("golden")); - sp_str_t dir = sp_fs_parent_path(sp_fs_get_exe_path(mem)); + sp_str_t exe = sp_zero; + sp_fs_get_exe_path(mem, &exe); + sp_str_t dir = sp_fs_parent_path(exe); while (!sp_str_empty(dir)) { sp_str_t candidate = sp_fs_join_path(mem, dir, sp_str_lit("test/cli/golden")); if (sp_fs_exists(candidate)) return candidate; @@ -49,7 +51,9 @@ static void run_cli_usage_test(s32* utest_result, sp_mem_t mem, cli_usage_test_t sp_str_t dir = cli_usage_golden_dir(mem); if (sp_str_empty(dir)) { - SP_TEST_REPORT("could not locate the golden directory from {}\n", sp_fmt_str(sp_fs_get_exe_path(mem))); + sp_str_t exe = sp_zero; + sp_fs_get_exe_path(mem, &exe); + SP_TEST_REPORT("could not locate the golden directory from {}\n", sp_fmt_str(exe)); SP_FAIL(); return; } diff --git a/test/fs/atomic.c b/test/fs/atomic.c index 91ff9878..ab9e9078 100644 --- a/test/fs/atomic.c +++ b/test/fs/atomic.c @@ -24,15 +24,14 @@ typedef struct { // the process root. static const test_t open_at_tests [] = { { - .name = "nested_parents_created", - .path = "A/B/C", - .content = "C", + .name = "missing_parent", + .path = "A/B", + .content = "B", .op = OP_REPLACE, .expect = { + .err = SP_ERR_SYS_NOT_FOUND, .paths = { - { .path = "A", .exists = true, .kind = SP_FS_KIND_DIR }, - { .path = "A/B", .exists = true, .kind = SP_FS_KIND_DIR }, - { .path = "A/B/C", .exists = true, .kind = SP_FS_KIND_FILE, .content = "C" }, + { .path = "A" }, }, }, }, @@ -52,12 +51,12 @@ static const test_t open_at_tests [] = { }, { .name = "exclusive_new", - .path = "A/B", + .path = "B", .content = "B", .op = OP_EXCLUSIVE, .expect = { .paths = { - { .path = "A/B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "B" }, + { .path = "B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "B" }, }, }, }, @@ -85,7 +84,12 @@ static const test_t open_at_tests [] = { .content = "B", .op = OP_REPLACE, .expect = { + // a file in the parent chain: POSIX reports ENOTDIR, NT reports PATH_NOT_FOUND +#if defined(SP_WIN32) + .err = SP_ERR_SYS_NOT_FOUND, +#else .err = SP_ERR_SYS_NOT_DIR, +#endif .paths = { { .path = "P", .exists = true, .kind = SP_FS_KIND_FILE, .content = "P" }, }, @@ -93,47 +97,54 @@ static const test_t open_at_tests [] = { }, { .name = "abort_leaves_nothing", - .path = "A/B", - .content = "B", + .path = "A", + .content = "A", .op = OP_ABORT, .expect = { .paths = { - { .path = "A", .exists = true, .kind = SP_FS_KIND_DIR }, - { .path = "A/B" }, + { .path = "A" }, }, }, }, }; // The temp lives in a staging directory "S" under the sandbox, so the -// destination's parent is first probed by the commit, not the open. +// destination's parent is first seen by the commit, not the open. static const test_t staged_tests [] = { { - .name = "parent_created_at_commit", - .path = "A/B/C", - .content = "C", + .name = "missing_parent_at_commit", + .setup = { + { "S", FS_SETUP_DIR }, + }, + .path = "A/B", + .content = "B", .op = OP_REPLACE, .expect = { + .err = SP_ERR_SYS_NOT_FOUND, .paths = { - { .path = "S", .exists = true, .kind = SP_FS_KIND_DIR }, - { .path = "A/B/C", .exists = true, .kind = SP_FS_KIND_FILE, .content = "C" }, + { .path = "A" }, }, }, }, { - .name = "exclusive_parent_created_at_commit", + .name = "exclusive_missing_parent_at_commit", + .setup = { + { "S", FS_SETUP_DIR }, + }, .path = "A/B", .content = "B", .op = OP_EXCLUSIVE, .expect = { + .err = SP_ERR_SYS_NOT_FOUND, .paths = { - { .path = "A/B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "B" }, + { .path = "A" }, }, }, }, { .name = "exclusive_existing", .setup = { + { "S", FS_SETUP_DIR }, { .path = "A", .content = "old" }, }, .path = "A", @@ -149,6 +160,7 @@ static const test_t staged_tests [] = { { .name = "parent_is_file", .setup = { + { "S", FS_SETUP_DIR }, { .path = "P", .content = "P" }, }, .path = "P/B", @@ -164,6 +176,7 @@ static const test_t staged_tests [] = { { .name = "exclusive_parent_is_file", .setup = { + { "S", FS_SETUP_DIR }, { .path = "P", .content = "P" }, }, .path = "P/B", @@ -185,7 +198,12 @@ static const test_t staged_tests [] = { .content = "A", .op = OP_REPLACE, .expect = { + // a file in the parent chain: POSIX reports ENOTDIR, NT reports PATH_NOT_FOUND +#if defined(SP_WIN32) + .err = SP_ERR_SYS_NOT_FOUND, +#else .err = SP_ERR_SYS_NOT_DIR, +#endif .paths = { { .path = "S", .exists = true, .kind = SP_FS_KIND_FILE, .content = "S" }, }, @@ -193,6 +211,9 @@ static const test_t staged_tests [] = { }, { .name = "abort_leaves_nothing", + .setup = { + { "S", FS_SETUP_DIR }, + }, .path = "A/B", .content = "B", .op = OP_ABORT, @@ -218,14 +239,14 @@ static sp_err_t drive(sp_fs_atomic_t* af, sp_err_t err, const test_t* it) { } sp_test_each(fs, atomic_open_at, test_t, open_at_tests) { - sp_str_t sandbox = sp_test_dir(t); + sp_path_t sandbox = sp_test_dir(t); fs_apply_setup(t, sandbox, it->setup); sp_sys_fd_t dir = SP_SYS_INVALID_FD; - sp_must_ok(t, sp_sys_open_dir_s(sp_sys_get_root(0), sandbox, &dir)); + sp_must_ok(t, sp_fs_open_dir_at(sandbox, &dir)); sp_fs_atomic_t af = sp_zero; - sp_err_t err = drive(&af, sp_fs_atomic_open_at(&af, dir, sp_str_view(it->path)), it); + sp_err_t err = drive(&af, sp_fs_atomic_open_at(&af, sp_path(dir, sp_str_view(it->path))), it); sp_sys_close(dir); sp_expect_err_eq(t, err, it->expect.err); @@ -235,26 +256,20 @@ sp_test_each(fs, atomic_open_at, test_t, open_at_tests) { } sp_test_each(fs, atomic_staged, test_t, staged_tests) { - sp_mem_t mem = sp_test_arena(t); - sp_str_t sandbox = sp_test_dir(t); + sp_path_t sandbox = sp_test_dir(t); fs_apply_setup(t, sandbox, it->setup); - sp_str_t path = sp_fs_join_path(mem, sandbox, sp_str_view(it->path)); - sp_str_t staging = sp_fs_join_path(mem, sandbox, sp_str_lit("S")); + sp_sys_fd_t dir = SP_SYS_INVALID_FD; + sp_must_ok(t, sp_fs_open_dir_at(sandbox, &dir)); + + sp_path_t path = sp_path(dir, sp_str_view(it->path)); sp_fs_atomic_t af = sp_zero; - sp_err_t err = drive(&af, sp_fs_atomic_open_staged(&af, path, staging), it); + sp_err_t err = drive(&af, sp_fs_atomic_open_staged_at(&af, path, sp_str_lit("S")), it); + sp_sys_close(dir); sp_expect_err_eq(t, err, it->expect.err); fs_expect_paths(t, sandbox, it->expect.paths); fs_expect_no_temps(t, sandbox); return SP_OK; } - -sp_test(fs, atomic_staged_empty_staging_is_bug) { - sp_str_t path = sp_fs_join_path(sp_test_arena(t), sp_test_dir(t), sp_str_lit("A")); - - sp_fs_atomic_t af = sp_zero; - sp_expect_err_eq(t, sp_fs_atomic_open_staged(&af, path, sp_str_lit("")), SP_ERR_SYS_BUG); - return SP_OK; -} diff --git a/test/fs/canonicalize.c b/test/fs/canonicalize.c index a253df48..5ddef11b 100644 --- a/test/fs/canonicalize.c +++ b/test/fs/canonicalize.c @@ -1,7 +1,7 @@ #include "fs.h" typedef struct { - bool empty; + sp_err_t err; bool no_trailing_slash; bool no_backslash; const c8* name; @@ -82,43 +82,24 @@ static const test_t tests [] = { }, }, { - .name = "nonexistent_returns_empty", + .name = "nonexistent_is_not_found", .input = "/this/path/does/not/exist/at/all", .expect = { - .empty = true, + .err = SP_ERR_SYS_NOT_FOUND, }, }, { .name = "empty_input", .input = "", .expect = { - .empty = true, - }, - }, - { - .name = "nonexistent_relative", - .input = "no_such_file.txt", - .expect = { - .empty = true, + .err = SP_ERR_SYS_NOT_FOUND, }, }, { .name = "nonexistent_with_dotdot", .input = "no_such_dir/../also_missing.txt", .expect = { - .empty = true, - }, - }, - { - .name = "result_is_normalized", - .setup = { - { "A", FS_SETUP_DIR }, - }, - .input = "A", - .expect = { - .no_trailing_slash = true, - .no_backslash = true, - .exists = true, + .err = SP_ERR_SYS_NOT_FOUND, }, }, { @@ -132,13 +113,14 @@ static const test_t tests [] = { .no_backslash = true, .name = "A", .exists = true, + .same_as = "A", }, }, { .name = "resolves_symlink_to_dir", .setup = { { "A", FS_SETUP_DIR }, - { .path = "L", .kind = FS_SETUP_SYMLINK, .target = "A" }, + { .path = "L", .kind = FS_SETUP_DIR_SYMLINK, .target = "A" }, }, .input = "L", .expect = { @@ -161,20 +143,6 @@ static const test_t tests [] = { .exists = true, }, }, - { - .name = "symlink_with_dotdot", - .setup = { - { "A" }, - { "B", FS_SETUP_DIR }, - { .path = "B/L", .kind = FS_SETUP_SYMLINK, .target = "A" }, - }, - .input = "B/L", - .expect = { - .no_backslash = true, - .name = "A", - .exists = true, - }, - }, { .name = "idempotent", .setup = { @@ -186,35 +154,25 @@ static const test_t tests [] = { .idempotent = true, }, }, - { - .name = "through_symlink", - .setup = { - { "A" }, - { .path = "L", .kind = FS_SETUP_SYMLINK, .target = "A" }, - }, - .input = "L", - .expect = { - .same_as = "A", - }, - }, }; sp_test_each(fs, canonicalize, test_t, tests) { sp_test_skip_on_wasm() - skip_if_symlinks_needed(t, it->setup); + if (fs_setup_needs_symlinks(it->setup)) sp_test_skip_without_symlinks(); sp_mem_t mem = sp_test_arena(t); - sp_str_t input = sp_str_view(it->input); - if (it->setup[0].path) { - sp_str_t sandbox = sp_test_dir(t); - fs_apply_setup(t, sandbox, it->setup); - input = sp_fs_join_path(mem, sandbox, input); - } + sp_path_t sandbox = sp_test_dir(t); + fs_apply_setup(t, sandbox, it->setup); + + sp_sys_fd_t dir = SP_SYS_INVALID_FD; + sp_must_ok(t, sp_fs_open_dir_at(sandbox, &dir)); - sp_str_t result = sp_fs_canonicalize_path(mem, input); + sp_path_t input = sp_path(dir, sp_str_view(it->input)); + sp_str_t result = sp_zero; + sp_expect_err_eq(t, sp_fs_canonicalize_path_at(mem, input, &result), it->expect.err); - if (it->expect.empty) { - if (result.len != 0) sp_test_fail(t, "expected empty, got {}", sp_fmt_str(result)); + if (it->expect.err) { + sp_sys_close(dir); return SP_OK; } @@ -229,15 +187,19 @@ sp_test_each(fs, canonicalize, test_t, tests) { sp_expect_str_eq_c(t, sp_fs_get_name(result), it->expect.name); } if (it->expect.exists) { - sp_expect(t, sp_fs_exists(result)); + sp_expect(t, sp_fs_exists_at(sp_path(dir, result))); } if (it->expect.idempotent) { - sp_expect_str_eq(t, sp_fs_canonicalize_path(mem, result), result); + sp_str_t again = sp_zero; + sp_expect_ok(t, sp_fs_canonicalize_path_at(mem, sp_path(dir, result), &again)); + sp_expect_str_eq(t, again, result); } if (it->expect.same_as) { - sp_str_t other = sp_fs_join_path(mem, sp_test_dir(t), sp_str_view(it->expect.same_as)); - sp_expect_str_eq(t, result, sp_fs_canonicalize_path(mem, other)); + sp_str_t other = sp_zero; + sp_expect_ok(t, sp_fs_canonicalize_path_at(mem, sp_path(dir, sp_str_view(it->expect.same_as)), &other)); + sp_expect_str_eq(t, result, other); } + sp_sys_close(dir); return SP_OK; } @@ -245,6 +207,10 @@ sp_test(fs, canon_dot_resolves_to_cwd) { sp_test_skip_on_wasm() sp_mem_t mem = sp_test_arena(t); - sp_expect_str_eq(t, sp_fs_canonicalize_path(mem, sp_str_lit(".")), sp_fs_get_cwd(mem)); + sp_str_t canonical = sp_zero; + sp_str_t cwd = sp_zero; + sp_must_ok(t, sp_fs_canonicalize_path(mem, sp_str_lit("."), &canonical)); + sp_must_ok(t, sp_fs_get_cwd_path(mem, &cwd)); + sp_expect_str_eq(t, canonical, cwd); return SP_OK; } diff --git a/test/fs/collect.c b/test/fs/collect.c index 3220bf72..12787f9d 100644 --- a/test/fs/collect.c +++ b/test/fs/collect.c @@ -55,9 +55,9 @@ static const test_t tests [] = { sp_test_each(fs, collect, test_t, tests) { sp_mem_t mem = sp_test_arena(t); - sp_str_t sandbox = sp_test_dir(t); + sp_path_t sandbox = sp_test_dir(t); fs_apply_setup(t, sandbox, it->setup); - sp_str_t root = sp_fs_join_path(mem, sandbox, sp_str_lit("R")); + sp_str_t root = sp_path_join(mem, sandbox, sp_str_lit("R")).sub; fs_match_t matches [FS_MAX_PATHS] = sp_zero; u32 n = 0; @@ -76,6 +76,7 @@ sp_test_each(fs, collect, test_t, tests) { sp_expect_eq(t, sp_da_size(results), (u64)n); sp_da_for(results, i) { sp_expect_str_eq(t, results[i].name, sp_fs_get_name(results[i].path)); + sp_expect_str_eq(t, sp_fs_join_path(mem, root, results[i].rel), results[i].path); fs_match(t, matches, n, results[i].path, results[i].kind); } fs_match_finish(t, matches, n); diff --git a/test/fs/copy.c b/test/fs/copy.c index a658f91b..4bdc2a4f 100644 --- a/test/fs/copy.c +++ b/test/fs/copy.c @@ -4,7 +4,6 @@ typedef enum { OP_COPY_FILE, OP_COPY_TREE, OP_COPY, - OP_COPY_INTO, } op_t; typedef struct { @@ -13,6 +12,7 @@ typedef struct { op_t op; sp_fs_atomic_mode_t mode; const c8* src; + const c8* dst_dir; const c8* dst; sp_err_t err; fs_expected_path_t expect [FS_MAX_PATHS]; @@ -55,15 +55,28 @@ static const test_t tests [] = { }, }, { - .name = "file_creates_parents", + .name = "file_missing_parent", .setup = { { .path = "A", .content = "A" }, }, .src = "A", - .dst = "D/E/B", + .dst = "D/B", + .err = SP_ERR_SYS_NOT_FOUND, .expect = { - { .path = "D/E", .exists = true, .kind = SP_FS_KIND_DIR }, - { .path = "D/E/B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "A" }, + { .path = "D" }, + }, + }, + { + .name = "file_across_dirs", + .setup = { + { .path = "A", .content = "A" }, + { "D", FS_SETUP_DIR }, + }, + .src = "A", + .dst_dir = "D", + .dst = "B", + .expect = { + { .path = "D/B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "A" }, }, }, { @@ -122,7 +135,7 @@ static const test_t tests [] = { .setup = { { .path = "A", .content = "A" }, { "D", FS_SETUP_DIR }, - { .path = "L", .kind = FS_SETUP_SYMLINK, .target = "D" }, + { .path = "L", .kind = FS_SETUP_DIR_SYMLINK, .target = "D" }, }, .src = "A", .dst = "L/B", @@ -156,18 +169,6 @@ static const test_t tests [] = { { .path = "B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "A" }, }, }, - { - .name = "file_source_dangling_symlink", - .setup = { - { .path = "L", .kind = FS_SETUP_SYMLINK, .target = "A" }, - }, - .src = "L", - .dst = "B", - .err = SP_ERR_SYS_NOT_FOUND, - .expect = { - { .path = "B" }, - }, - }, { .name = "file_source_missing", .setup = { @@ -193,20 +194,6 @@ static const test_t tests [] = { { .path = "B" }, }, }, -#if defined(SP_POSIX) - { - .name = "file_source_is_fifo", - .setup = { - { .path = "F", .kind = FS_SETUP_FIFO }, - }, - .src = "F", - .dst = "B", - .err = SP_ERR_SYS_UNSUPPORTED, - .expect = { - { .path = "B" }, - }, - }, -#endif { .name = "file_dest_parent_is_file", .setup = { @@ -215,7 +202,12 @@ static const test_t tests [] = { }, .src = "A", .dst = "P/B", + // a file in the parent chain: POSIX reports ENOTDIR, NT reports PATH_NOT_FOUND +#if defined(SP_WIN32) + .err = SP_ERR_SYS_NOT_FOUND, +#else .err = SP_ERR_SYS_NOT_DIR, +#endif .expect = { { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE, .content = "A" }, { .path = "P", .exists = true, .kind = SP_FS_KIND_FILE, .content = "P" }, @@ -277,6 +269,49 @@ static const test_t tests [] = { { .path = "E/B", .exists = true, .kind = SP_FS_KIND_DIR }, }, }, + { + .name = "tree_creates_parents", + .setup = { + { "A", FS_SETUP_DIR }, + { .path = "A/B", .content = "B" }, + }, + .op = OP_COPY_TREE, + .src = "A", + .dst = "D/E", + .expect = { + { .path = "D/E/B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "B" }, + }, + }, + { + .name = "tree_across_dirs", + .setup = { + { "A", FS_SETUP_DIR }, + { .path = "A/B", .content = "B" }, + { "D", FS_SETUP_DIR }, + }, + .op = OP_COPY_TREE, + .src = "A", + .dst_dir = "D", + .dst = "E", + .expect = { + { .path = "D/E/B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "B" }, + }, + }, + { + .name = "tree_into_open_dir", + .setup = { + { "A", FS_SETUP_DIR }, + { .path = "A/B", .content = "B" }, + { "D", FS_SETUP_DIR }, + }, + .op = OP_COPY_TREE, + .src = "A", + .dst_dir = "D", + .dst = ".", + .expect = { + { .path = "D/B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "B" }, + }, + }, { .name = "tree_replaces_existing", .setup = { @@ -314,59 +349,63 @@ static const test_t tests [] = { .setup = { { "A", FS_SETUP_DIR }, { .path = "A/C", .content = "C" }, - { .path = "A/L", .kind = FS_SETUP_SYMLINK, .target = "A/C" }, + { .path = "A/L", .kind = FS_SETUP_SYMLINK, .target = "C" }, }, .op = OP_COPY_TREE, .src = "A", .dst = "E", .expect = { { .path = "E/C", .exists = true, .kind = SP_FS_KIND_FILE, .content = "C" }, - { .path = "E/L", .exists = true, .kind = SP_FS_KIND_SYMLINK, .target = "A/C" }, + { .path = "E/L", .exists = true, .kind = SP_FS_KIND_SYMLINK, .target = "C" }, }, }, { - .name = "tree_symlink_replaced", + .name = "tree_symlink_to_dir_preserved", .setup = { - { "A", FS_SETUP_DIR }, - { .path = "A/C", .content = "C" }, - { .path = "A/L", .kind = FS_SETUP_SYMLINK, .target = "A/C" }, - { "E", FS_SETUP_DIR }, - { .path = "E/L", .kind = FS_SETUP_SYMLINK, .target = "A" }, + { .path = "A/Z/F", .content = "F" }, + { .path = "A/L", .kind = FS_SETUP_DIR_SYMLINK, .target = "Z" }, }, .op = OP_COPY_TREE, .src = "A", .dst = "E", .expect = { - { .path = "E/L", .exists = true, .kind = SP_FS_KIND_SYMLINK, .target = "A/C" }, + { .path = "E/Z/F", .exists = true, .kind = SP_FS_KIND_FILE, .content = "F" }, + { .path = "E/L", .exists = true, .kind = SP_FS_KIND_SYMLINK, .target = "Z" }, + { .path = "E/L/F", .exists = true, .kind = SP_FS_KIND_FILE, .content = "F" }, }, }, { - .name = "tree_into_itself", + .name = "tree_symlink_replaced", .setup = { { "A", FS_SETUP_DIR }, { .path = "A/C", .content = "C" }, + { .path = "A/L", .kind = FS_SETUP_SYMLINK, .target = "C" }, + { "E", FS_SETUP_DIR }, + { .path = "E/L", .kind = FS_SETUP_DIR_SYMLINK, .target = "../A" }, }, .op = OP_COPY_TREE, .src = "A", - .dst = "A/B", - .err = SP_ERR_SYS_INVALID, + .dst = "E", .expect = { - { .path = "A/B" }, + { .path = "E/L", .exists = true, .kind = SP_FS_KIND_SYMLINK, .target = "C" }, }, }, { - .name = "tree_into_itself_through_symlink", + .name = "tree_symlink_exclusive_existing", .setup = { { "A", FS_SETUP_DIR }, { .path = "A/C", .content = "C" }, - { .path = "L", .kind = FS_SETUP_SYMLINK, .target = "A" }, + { .path = "A/L", .kind = FS_SETUP_SYMLINK, .target = "C" }, + { .path = "E/F", .content = "F" }, + { .path = "E/L", .kind = FS_SETUP_SYMLINK, .target = "F" }, }, .op = OP_COPY_TREE, + .mode = SP_FS_ATOMIC_EXCLUSIVE, .src = "A", - .dst = "L/B", - .err = SP_ERR_SYS_INVALID, + .dst = "E", + .err = SP_ERR_SYS_EXISTS, .expect = { - { .path = "A/B" }, + { .path = "E/L", .exists = true, .kind = SP_FS_KIND_SYMLINK, .target = "F" }, }, }, { @@ -378,24 +417,10 @@ static const test_t tests [] = { .op = OP_COPY_TREE, .src = "A", .dst = "A", - .err = SP_ERR_SYS_INVALID, .expect = { { .path = "A/C", .exists = true, .kind = SP_FS_KIND_FILE, .content = "C" }, }, }, - { - .name = "tree_beside_itself", - .setup = { - { "A", FS_SETUP_DIR }, - { .path = "A/C", .content = "C" }, - }, - .op = OP_COPY_TREE, - .src = "A", - .dst = "A/../E", - .expect = { - { .path = "E/C", .exists = true, .kind = SP_FS_KIND_FILE, .content = "C" }, - }, - }, { .name = "tree_dest_parent_is_file", .setup = { @@ -501,53 +526,33 @@ static const test_t tests [] = { { .path = "B" }, }, }, - { - .name = "into_dir", - .setup = { - { "A", FS_SETUP_DIR }, - { .path = "A/B", .content = "B" }, - { "D", FS_SETUP_DIR }, - }, - .op = OP_COPY_INTO, - .src = "A", - .dst = "D", - .expect = { - { .path = "D/A", .exists = true, .kind = SP_FS_KIND_DIR }, - { .path = "D/A/B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "B" }, - }, - }, - { - .name = "into_file", - .setup = { - { .path = "A", .content = "A" }, - { "D", FS_SETUP_DIR }, - }, - .op = OP_COPY_INTO, - .src = "A", - .dst = "D", - .expect = { - { .path = "D/A", .exists = true, .kind = SP_FS_KIND_FILE, .content = "A" }, - }, - }, }; sp_test_each(fs, copy, test_t, tests) { - skip_if_symlinks_needed(t, it->setup); + if (fs_setup_needs_symlinks(it->setup)) sp_test_skip_without_symlinks(); sp_mem_t mem = sp_test_arena(t); - sp_str_t sandbox = sp_test_dir(t); + sp_path_t sandbox = sp_test_dir(t); fs_apply_setup(t, sandbox, it->setup); - sp_str_t src = sp_fs_join_path(mem, sandbox, sp_cstr_as_str(it->src)); - sp_str_t dst = sp_fs_join_path(mem, sandbox, sp_cstr_as_str(it->dst)); + sp_sys_fd_t dir = SP_SYS_INVALID_FD; + sp_must_ok(t, sp_fs_open_dir_at(sandbox, &dir)); + + sp_path_t dst_base = it->dst_dir ? sp_path_join(mem, sandbox, sp_cstr_as_str(it->dst_dir)) : sandbox; + sp_sys_fd_t dst_dir = SP_SYS_INVALID_FD; + sp_must_ok(t, sp_fs_open_dir_at(dst_base, &dst_dir)); + + sp_path_t src = sp_path(dir, sp_cstr_as_str(it->src)); + sp_path_t dst = sp_path(dst_dir, sp_cstr_as_str(it->dst)); sp_err_t result = SP_OK; switch (it->op) { - case OP_COPY_FILE: result = sp_fs_copy_file(src, dst, it->mode); break; - case OP_COPY_TREE: result = sp_fs_copy_tree(src, dst, it->mode); break; - case OP_COPY: result = sp_fs_copy(src, dst); break; - case OP_COPY_INTO: result = sp_fs_copy_into(src, dst); break; + case OP_COPY_FILE: result = sp_fs_copy_file_at(src, dst, it->mode); break; + case OP_COPY_TREE: result = sp_fs_copy_tree_at(src, dst, it->mode); break; + case OP_COPY: result = sp_fs_copy_at(src, dst, it->mode); break; } + sp_sys_close(dst_dir); + sp_sys_close(dir); sp_expect_err_eq(t, result, it->err); fs_expect_paths(t, sandbox, it->expect); @@ -569,105 +574,111 @@ static const size_test_t sizes [] = { sp_test_each(fs, copy_size, size_test_t, sizes) { sp_mem_t mem = sp_test_arena(t); - sp_str_t from = fs_path_c(t, "A"); - sp_str_t to = fs_path_c(t, "B"); + sp_path_t sandbox = sp_test_dir(t); + sp_path_t from = sp_path_join(mem, sandbox, sp_str_lit("A")); + sp_path_t to = sp_path_join(mem, sandbox, sp_str_lit("B")); u8* data = sp_alloc_n(mem, u8, it->size); sp_for(i, it->size) { data[i] = (u8)(i * 31 + 7); } - sp_must_ok(t, sp_fs_create_file_slice(from, sp_mem_slice(data, it->size))); - sp_must_ok(t, sp_fs_copy_file(from, to, SP_FS_ATOMIC_REPLACE)); + sp_must_ok(t, sp_fs_create_file_slice_at(from, sp_mem_slice(data, it->size))); + sp_must_ok(t, sp_fs_copy_file_at(from, to, SP_FS_ATOMIC_REPLACE)); sp_str_t copied = sp_zero; - sp_must_ok(t, sp_io_read_file(mem, to, &copied)); + sp_must_ok(t, sp_io_read_file_at(mem, to, &copied)); sp_must_eq(t, copied.len, it->size); sp_expect_mem_eq(t, copied.data, data, it->size); return SP_OK; } #if defined(SP_POSIX) +static sp_sys_file_perms_t mode(u32 value) { + return (sp_sys_file_perms_t) { .value = value }; +} + // postcondition: dest has source's bytes and mode, on a fresh inode; timestamps unspecified sp_test(fs, copy_file_preserves_mode, .serial = true) { sp_mem_t mem = sp_test_arena(t); - sp_str_t from = fs_path_c(t, "A"); - sp_str_t to = fs_path_c(t, "B"); - sp_fs_create_file_str(from, sp_str_lit("A")); - - sp_must_eq(t, chmod(sp_cstr_from_str(mem, from), 0755), 0); + sp_path_t sandbox = sp_test_dir(t); + sp_path_t from = sp_path_join(mem, sandbox, sp_str_lit("A")); + sp_path_t to = sp_path_join(mem, sandbox, sp_str_lit("B")); + sp_must_ok(t, sp_fs_create_file_str_at(from, sp_str_lit("A"))); + sp_must_ok(t, sp_sys_set_file_perms_s(from.dir, from.sub, mode(0755))); - struct stat from_stat = sp_zero; - sp_must_eq(t, stat(sp_cstr_from_str(mem, from), &from_stat), 0); + sp_sys_file_meta_t from_meta = sp_zero; + sp_must_ok(t, sp_sys_get_path_metadata_s(from.dir, from.sub, &from_meta)); mode_t mask = umask(077); - sp_err_t copied_err = sp_fs_copy_file(from, to, SP_FS_ATOMIC_REPLACE); + sp_err_t copied_err = sp_fs_copy_file_at(from, to, SP_FS_ATOMIC_REPLACE); umask(mask); sp_must_ok(t, copied_err); - struct stat to_stat = sp_zero; - sp_must_eq(t, stat(sp_cstr_from_str(mem, to), &to_stat), 0); - sp_must_eq(t, to_stat.st_mode, from_stat.st_mode); - sp_must_eq(t, to_stat.st_size, from_stat.st_size); - sp_must(t, to_stat.st_ino != from_stat.st_ino); + sp_sys_file_meta_t to_meta = sp_zero; + sp_must_ok(t, sp_sys_get_path_metadata_s(to.dir, to.sub, &to_meta)); + sp_must_eq(t, to_meta.perms.value, from_meta.perms.value); + sp_must_eq(t, to_meta.size, from_meta.size); + sp_must(t, to_meta.id != from_meta.id); sp_str_t copied = sp_zero; - sp_io_read_file(mem, to, &copied); + sp_io_read_file_at(mem, to, &copied); sp_expect_str_eq(t, copied, sp_str_lit("A")); return SP_OK; } sp_test(fs, copy_file_replaces_read_only_dest) { sp_mem_t mem = sp_test_arena(t); - sp_str_t from = fs_path_c(t, "A"); - sp_str_t to = fs_path_c(t, "B"); - sp_fs_create_file_str(from, sp_str_lit("A")); - sp_fs_create_file_str(to, sp_str_lit("B")); - sp_must_eq(t, chmod(sp_cstr_from_str(mem, to), 0444), 0); + sp_path_t sandbox = sp_test_dir(t); + sp_path_t from = sp_path_join(mem, sandbox, sp_str_lit("A")); + sp_path_t to = sp_path_join(mem, sandbox, sp_str_lit("B")); + sp_must_ok(t, sp_fs_create_file_str_at(from, sp_str_lit("A"))); + sp_must_ok(t, sp_fs_create_file_str_at(to, sp_str_lit("B"))); + sp_must_ok(t, sp_sys_set_file_perms_s(to.dir, to.sub, mode(0444))); - sp_must_ok(t, sp_fs_copy_file(from, to, SP_FS_ATOMIC_REPLACE)); + sp_must_ok(t, sp_fs_copy_file_at(from, to, SP_FS_ATOMIC_REPLACE)); sp_str_t copied = sp_zero; - sp_io_read_file(mem, to, &copied); + sp_io_read_file_at(mem, to, &copied); sp_expect_str_eq(t, copied, sp_str_lit("A")); return SP_OK; } sp_test(fs, copy_file_unwritable_parent_leaves_nothing) { sp_mem_t mem = sp_test_arena(t); - sp_str_t from = fs_path_c(t, "A"); - sp_str_t dir = fs_path_c(t, "D"); - sp_str_t to = fs_path_c(t, "D/B"); - sp_fs_create_file_str(from, sp_str_lit("A")); - sp_must_ok(t, sp_fs_create_dir(dir)); + sp_path_t sandbox = sp_test_dir(t); + sp_path_t from = sp_path_join(mem, sandbox, sp_str_lit("A")); + sp_path_t dir = sp_path_join(mem, sandbox, sp_str_lit("D")); + sp_path_t to = sp_path_join(mem, sandbox, sp_str_lit("D/B")); + sp_must_ok(t, sp_fs_create_file_str_at(from, sp_str_lit("A"))); + sp_must_ok(t, sp_fs_create_dir_at(dir)); - const c8* dir_c = sp_cstr_from_str(mem, dir); - sp_must_eq(t, chmod(dir_c, 0555), 0); - sp_err_t result = sp_fs_copy_file(from, to, SP_FS_ATOMIC_REPLACE); - sp_must_eq(t, chmod(dir_c, 0755), 0); + sp_must_ok(t, sp_sys_set_file_perms_s(dir.dir, dir.sub, mode(0555))); + sp_err_t result = sp_fs_copy_file_at(from, to, SP_FS_ATOMIC_REPLACE); + sp_must_ok(t, sp_sys_set_file_perms_s(dir.dir, dir.sub, mode(0755))); if (!result) return sp_test_skip(t, "directory permissions not enforced"); sp_expect_err_eq(t, result, SP_ERR_SYS_ACCESS_DENIED); - sp_expect(t, !sp_fs_exists(to)); - fs_expect_no_temps(t, sp_test_dir(t)); + sp_expect(t, !sp_fs_exists_at(to)); + fs_expect_no_temps(t, sandbox); return SP_OK; } sp_test(fs, copy_tree_unreadable_subdir_fails) { sp_mem_t mem = sp_test_arena(t); - sp_str_t from = fs_path_c(t, "A"); - sp_str_t locked = fs_path_c(t, "A/B"); - sp_str_t to = fs_path_c(t, "E"); - sp_must_ok(t, sp_fs_create_dir(locked)); - sp_must_ok(t, sp_fs_create_file_str(fs_path_c(t, "A/B/C"), sp_str_lit("C"))); + sp_path_t sandbox = sp_test_dir(t); + sp_path_t from = sp_path_join(mem, sandbox, sp_str_lit("A")); + sp_path_t locked = sp_path_join(mem, sandbox, sp_str_lit("A/B")); + sp_path_t to = sp_path_join(mem, sandbox, sp_str_lit("E")); + sp_must_ok(t, sp_fs_create_dir_at(locked)); + sp_must_ok(t, sp_fs_create_file_str_at(sp_path_join(mem, sandbox, sp_str_lit("A/B/C")), sp_str_lit("C"))); - const c8* locked_c = sp_cstr_from_str(mem, locked); - sp_must_eq(t, chmod(locked_c, 0), 0); - sp_err_t result = sp_fs_copy_tree(from, to, SP_FS_ATOMIC_REPLACE); - sp_must_eq(t, chmod(locked_c, 0755), 0); + sp_must_ok(t, sp_sys_set_file_perms_s(locked.dir, locked.sub, mode(0))); + sp_err_t result = sp_fs_copy_tree_at(from, to, SP_FS_ATOMIC_REPLACE); + sp_must_ok(t, sp_sys_set_file_perms_s(locked.dir, locked.sub, mode(0755))); if (!result) return sp_test_skip(t, "directory permissions not enforced"); sp_expect_err_eq(t, result, SP_ERR_SYS_ACCESS_DENIED); - sp_expect(t, !sp_fs_exists(fs_path_c(t, "E/B/C"))); + sp_expect(t, !sp_fs_exists_at(sp_path_join(mem, sandbox, sp_str_lit("E/B/C")))); return SP_OK; } #endif diff --git a/test/fs/create_dir.c b/test/fs/create_dir.c index 0a90e7e8..accbba3b 100644 --- a/test/fs/create_dir.c +++ b/test/fs/create_dir.c @@ -76,7 +76,7 @@ static const test_t tests [] = { .name = "destination_is_symlink_to_directory", .setup = { { "A", FS_SETUP_DIR }, - { .path = "L", .kind = FS_SETUP_SYMLINK, .target = "A" }, + { .path = "L", .kind = FS_SETUP_DIR_SYMLINK, .target = "A" }, }, .target = "L", .expect = { @@ -90,7 +90,7 @@ static const test_t tests [] = { .name = "destination_under_symlink_to_directory", .setup = { { "A", FS_SETUP_DIR }, - { .path = "L", .kind = FS_SETUP_SYMLINK, .target = "A" }, + { .path = "L", .kind = FS_SETUP_DIR_SYMLINK, .target = "A" }, }, .target = "L/B", .expect = { @@ -131,32 +131,14 @@ static const test_t tests [] = { }; sp_test_each(fs, create_dir, test_t, tests) { - skip_if_symlinks_needed(t, it->setup); + if (fs_setup_needs_symlinks(it->setup)) sp_test_skip_without_symlinks(); - sp_str_t sandbox = sp_test_dir(t); + sp_path_t sandbox = sp_test_dir(t); fs_apply_setup(t, sandbox, it->setup); - sp_str_t target = sp_fs_join_path(sp_test_arena(t), sandbox, sp_str_view(it->target)); - sp_expect_err_eq(t, sp_fs_create_dir(target), it->expect.err); + sp_path_t target = sp_path_join(sp_test_arena(t), sandbox, sp_cstr_as_str(it->target)); + sp_expect_err_eq(t, sp_fs_create_dir_at(target), it->expect.err); fs_expect_paths(t, sandbox, it->expect.paths); return SP_OK; } - -sp_test(fs, create_dir_create_multi_level_relative, .serial = true) { - sp_test_skip_on_wasm() - - sp_mem_t mem = sp_test_arena(t); - sp_str_t sandbox = sp_test_dir(t); - - sp_str_t cwd = sp_fs_get_cwd(mem); - sp_must_ok(t, sp_sys_chdir_s(sandbox)); - sp_err_t result = sp_fs_create_dir(sp_str_lit("A/B")); - sp_must_ok(t, sp_sys_chdir_s(cwd)); - - sp_expect_ok(t, result); - sp_str_t created = sp_fs_join_path(mem, sandbox, sp_str_lit("A/B")); - sp_expect(t, sp_fs_exists(created)); - sp_expect_eq(t, sp_fs_get_kind(created), SP_FS_KIND_DIR); - return SP_OK; -} diff --git a/test/fs/create_dir_sim.c b/test/fs/create_dir_sim.c new file mode 100644 index 00000000..97f6f3ee --- /dev/null +++ b/test/fs/create_dir_sim.c @@ -0,0 +1,40 @@ +#include "sp.h" +#include "sp/sp_test.h" + +#define MAX_MKDIRS 64 + +typedef struct { + const c8* name; + const c8* target; +} test_t; + +static const test_t tests [] = { + { .name = "bare", .target = "A" }, + { .name = "nested", .target = "A/B" }, + { .name = "under_root", .target = "/A" }, + { .name = "dot", .target = "." }, + { .name = "root", .target = "/" }, + { .name = "empty", .target = "" }, +}; + +static u32 mkdirs; + +static sp_err_t mkdir_missing(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_perms_t perms) { + mkdirs++; + return mkdirs > MAX_MKDIRS ? SP_ERR_SYS_LOOP : SP_ERR_SYS_NOT_FOUND; +} + +static sp_err_t run(sp_test_t* t, test_t* c) { + sp_sys_vtable_t vt = sp_sys_vtable_platform; + vt.mkdir = mkdir_missing; + + mkdirs = 0; + const sp_sys_vtable_t* saved = sp_sys_set_vtable(&vt); + sp_err_t err = sp_fs_create_dir_at(sp_path_cwd(sp_cstr_as_str(c->target))); + sp_sys_set_vtable(saved); + + sp_expect_err_eq(t, err, SP_ERR_SYS_NOT_FOUND); + return SP_OK; +} + +sp_test_each_fn(fs, create_dir_sim, test_t, tests, run, .serial = true); diff --git a/test/fs/create_file.c b/test/fs/create_file.c index 01481848..4af364ae 100644 --- a/test/fs/create_file.c +++ b/test/fs/create_file.c @@ -71,26 +71,26 @@ static const test_t tests [] = { }; sp_test_each(fs, create_file, test_t, tests) { - sp_str_t sandbox = sp_test_dir(t); + sp_path_t sandbox = sp_test_dir(t); fs_apply_setup(t, sandbox, it->setup); - sp_str_t path = sp_fs_join_path(sp_test_arena(t), sandbox, sp_str_view(it->path)); + sp_path_t path = sp_path_join(sp_test_arena(t), sandbox, sp_str_view(it->path)); sp_err_t result = SP_OK; switch (it->variant) { case VARIANT_EMPTY: { - result = sp_fs_create_file(path); + result = sp_fs_create_file_at(path); break; } case VARIANT_SLICE: { - result = sp_fs_create_file_slice(path, sp_mem_slice((u8*)it->content, sp_cstr_len(it->content))); + result = sp_fs_create_file_slice_at(path, sp_mem_slice((u8*)it->content, sp_cstr_len(it->content))); break; } case VARIANT_STR: { - result = sp_fs_create_file_str(path, sp_str_view(it->content)); + result = sp_fs_create_file_str_at(path, sp_str_view(it->content)); break; } case VARIANT_CSTR: { - result = sp_fs_create_file_cstr(path, it->content); + result = sp_fs_create_file_cstr_at(path, it->content); break; } } diff --git a/test/fs/dir/os.c b/test/fs/dir/os.c index 6647b2cd..bd1918d5 100644 --- a/test/fs/dir/os.c +++ b/test/fs/dir/os.c @@ -88,13 +88,13 @@ static const test_t tests [] = { }; sp_test_each(fs, dir, test_t, tests) { - skip_if_symlinks_needed(t, it->setup); + if (fs_setup_needs_symlinks(it->setup)) sp_test_skip_without_symlinks(); sp_mem_t mem = sp_test_arena(t); - sp_str_t sandbox = sp_test_dir(t); + sp_path_t sandbox = sp_test_dir(t); fs_apply_setup(t, sandbox, it->setup); - sp_str_t dir = it->dir ? sp_fs_join_path(mem, sandbox, sp_cstr_as_str(it->dir)) : sandbox; + sp_path_t dir = it->dir ? sp_path_join(mem, sandbox, sp_cstr_as_str(it->dir)) : sandbox; fs_match_t* matches = sp_alloc_n(mem, fs_match_t, FS_MAX_PATHS + it->bulk); u32 n = 0; @@ -104,33 +104,32 @@ sp_test_each(fs, dir, test_t, tests) { } sp_for(b, it->bulk) { sp_str_t name = sp_fmt(mem, "{}{}", sp_fmt_cstr(BULK_PREFIX), sp_fmt_uint(b)).value; - sp_expect_ok(t, sp_fs_create_file(sp_fs_join_path(mem, dir, name))); + sp_expect_ok(t, sp_fs_create_file_at(sp_path_join(mem, dir, name))); matches[n++] = (fs_match_t) { .key = name, .kind = SP_FS_KIND_FILE }; } sp_sys_fd_t sandbox_fd = SP_SYS_INVALID_FD; if (it->relative) { - sp_try(sp_sys_open_dir_s(sp_sys_get_root(0), sandbox, &sandbox_fd)); + sp_try(sp_fs_open_dir_at(sandbox, &sandbox_fd)); } SP_ALIGNED u8 buf [SP_SYS_DIR_MIN_BUF]; - sp_fs_dir_t iter = sp_zero; - sp_err_t open_err = it->relative - ? sp_fs_dir_open(&iter, sandbox_fd, sp_cstr_as_str(it->dir), sp_mem_slice(buf, sizeof(buf))) - : sp_fs_dir_open(&iter, sp_sys_get_root(0), dir, sp_mem_slice(buf, sizeof(buf))); + sp_fs_dir_it_t iter = sp_zero; + sp_path_t opened = it->relative ? sp_path(sandbox_fd, sp_cstr_as_str(it->dir)) : dir; + sp_err_t open_err = sp_fs_dir_it_open(&iter, opened, 0, sp_mem_slice(buf, sizeof(buf))); sp_expect_ok(t, open_err); if (!open_err) { sp_err_t walk = SP_OK; while (true) { sp_fs_dir_entry_t entry = sp_zero; - walk = sp_fs_dir_next(&iter, &entry); + walk = sp_fs_dir_it_next(&iter, &entry); if (walk) break; if (!entry.name.data) break; fs_match(t, matches, n, entry.name, entry.kind); } sp_expect_ok(t, walk); - sp_expect_ok(t, sp_fs_dir_close(&iter)); + sp_expect_ok(t, sp_fs_dir_it_close(&iter)); } fs_match_finish(t, matches, n); diff --git a/test/fs/dir/sim.c b/test/fs/dir/sim.c index efb54b56..c6f6139e 100644 --- a/test/fs/dir/sim.c +++ b/test/fs/dir/sim.c @@ -25,9 +25,9 @@ static const test_t tests [] = { }, }, { - .name = "closes_fd_when_from_fd_fails", + .name = "closes_fd_when_it_open_fails", .dirs = { - { .path = "T", .from_fd = SP_ERR_SYS_IO }, + { .path = "T", .it_open = SP_ERR_SYS_IO }, }, .expect = { .open = SP_ERR_SYS_IO, @@ -41,19 +41,19 @@ sp_test_each(fs, dir_sim, test_t, tests, .serial = true) { sim_begin(&s, it->dirs); SP_ALIGNED u8 buf [SP_SYS_DIR_MIN_BUF]; - sp_fs_dir_t dir = sp_zero; - sp_err_t open_err = sp_fs_dir_open(&dir, sp_sys_get_root(0), sp_str_lit("T"), sp_mem_slice(buf, sizeof(buf))); + sp_fs_dir_it_t dir = sp_zero; + sp_err_t open_err = sp_fs_dir_it_open(&dir, sp_path(sp_sys_get_root(0), sp_str_lit("T")), 0, sp_mem_slice(buf, sizeof(buf))); sp_expect_err_eq(t, open_err, it->expect.open); if (!open_err) { sp_err_t walk = SP_OK; while (true) { sp_fs_dir_entry_t entry = sp_zero; - walk = sp_fs_dir_next(&dir, &entry); + walk = sp_fs_dir_it_next(&dir, &entry); if (walk || !entry.name.data) break; } sp_expect_err_eq(t, walk, it->expect.walk); - sp_expect_ok(t, sp_fs_dir_close(&dir)); + sp_expect_ok(t, sp_fs_dir_it_close(&dir)); } sim_end(&s); diff --git a/test/fs/fd_relative.c b/test/fs/fd_relative.c index 388e1ad3..66be8270 100644 --- a/test/fs/fd_relative.c +++ b/test/fs/fd_relative.c @@ -110,16 +110,15 @@ sp_test_each(fs, fd_relative, test_t, tests) { #if !defined(SP_WIN32) if (it->win32) return sp_test_skip(t, "windows only"); #endif + if (it->op == OP_OPEN_ABS) sp_test_skip_without_absolute_names(); + sp_mem_t mem = sp_test_arena(t); - sp_str_t sandbox = sp_test_dir(t); + sp_path_t sandbox = sp_test_dir(t); fs_apply_setup(t, sandbox, it->setup); - sp_str_t cwd_path = sp_fs_join_path(mem, sandbox, sp_str_view(it->cwd)); + sp_path_t cwd_path = sp_path_join(mem, sandbox, sp_str_view(it->cwd)); sp_sys_fd_t cwd = SP_SYS_INVALID_FD; - if (sp_sys_open_dir_s(sp_sys_get_root(0), cwd_path, &cwd) != SP_OK) { - sp_test_fail(t, "failed to open dir {}", sp_fmt_str(cwd_path)); - return SP_ERR; - } + sp_must_ok(t, sp_fs_open_dir_at(cwd_path, &cwd)); switch (it->op) { case OP_STAT: { @@ -129,9 +128,8 @@ sp_test_each(fs, fd_relative, test_t, tests) { } case OP_OPEN: case OP_OPEN_ABS: { - sp_str_t path = it->op == OP_OPEN_ABS - ? sp_fs_join_path(mem, sandbox, sp_str_view(it->path)) - : sp_str_view(it->path); + sp_str_t path = sp_str_view(it->path); + if (it->op == OP_OPEN_ABS) sp_must_ok(t, sp_fs_canonicalize_path_at(mem, sp_path_join(mem, sandbox, path), &path)); sp_sys_fd_t fd = SP_SYS_INVALID_FD; sp_expect_ok(t, sp_sys_open_s(cwd, path, SP_SYS_OPEN_MODE_RO, 0, &fd)); diff --git a/test/fs/fs.h b/test/fs/fs.h index 32cc7dfc..15b79d0f 100644 --- a/test/fs/fs.h +++ b/test/fs/fs.h @@ -12,48 +12,6 @@ #define FS_MAX_PATHS 8 -///////////// -// SANDBOX // -///////////// -static sp_str_t fs_path(sp_test_t* t, sp_str_t relative) { - return sp_fs_join_path(sp_test_arena(t), sp_test_dir(t), relative); -} - -static sp_str_t fs_path_c(sp_test_t* t, const c8* relative) { - return fs_path(t, sp_cstr_as_str(relative)); -} - - -/////////////////// -// SYMLINK PROBE // -/////////////////// -static sp_test_once_t fs_symlink_probe = sp_zero; - -static sp_err_t fs_probe_symlinks(void* user) { - sp_str_t dir = *(sp_str_t*)user; - - sp_mem_arena_marker_t scratch = sp_mem_begin_scratch(); - sp_str_t target = sp_fs_join_path(scratch.mem, dir, sp_str_lit("probe_target")); - sp_str_t link = sp_fs_join_path(scratch.mem, dir, sp_str_lit("probe_link")); - - sp_fs_create_file(target); - sp_err_t err = sp_fs_create_sym_link(target, link); - if (!err) sp_fs_remove_file(link); - sp_fs_remove_file(target); - - sp_mem_end_scratch(scratch); - return err; -} - -static bool fs_symlinks_available(sp_test_t* t) { - sp_str_t dir = sp_test_dir(t); - return sp_test_once(&fs_symlink_probe, fs_probe_symlinks, &dir) == SP_OK; -} - -#define fs_skip_if_no_symlinks(t) \ - do { if (!fs_symlinks_available(t)) return sp_test_skip(t, "symlinks not available"); } while (0) - - //////////////////// // SHARED HARNESS // //////////////////// @@ -61,6 +19,7 @@ typedef enum { FS_SETUP_FILE, FS_SETUP_DIR, FS_SETUP_SYMLINK, + FS_SETUP_DIR_SYMLINK, FS_SETUP_FIFO, } fs_setup_kind_t; @@ -84,14 +43,11 @@ typedef struct { static bool fs_setup_needs_symlinks(const fs_setup_t setup [FS_MAX_SETUP]) { sp_for(it, FS_MAX_SETUP) { if (!setup[it].path) break; - if (setup[it].kind == FS_SETUP_SYMLINK) return true; + if (setup[it].kind == FS_SETUP_SYMLINK || setup[it].kind == FS_SETUP_DIR_SYMLINK) return true; } return false; } -#define skip_if_symlinks_needed(T, SETUP) \ - do { if (fs_setup_needs_symlinks(SETUP)) fs_skip_if_no_symlinks(T); } while (0) - static void fs_expect_bool(sp_test_t* t, sp_str_t path, const c8* label, bool actual, bool expected) { if (actual == expected) return; @@ -141,83 +97,84 @@ static void fs_match_finish(sp_test_t* t, fs_match_t* matches, u32 n) { } } -static void fs_apply_setup(sp_test_t* t, sp_str_t sandbox, const fs_setup_t setup [FS_MAX_SETUP]) { +static void fs_apply_setup(sp_test_t* t, sp_path_t sandbox, const fs_setup_t setup [FS_MAX_SETUP]) { sp_mem_t mem = sp_test_arena(t); sp_for(it, FS_MAX_SETUP) { const fs_setup_t* ent = &setup[it]; if (!ent->path) break; - sp_str_t path = sp_fs_join_path(mem, sandbox, sp_str_view(ent->path)); - sp_str_t parent = sp_fs_parent_path(path); - - if (!sp_str_empty(parent) && !sp_str_equal(parent, path) && !sp_fs_exists(parent)) { - sp_fs_create_dir(parent); - } + sp_path_t path = sp_path_join(mem, sandbox, sp_cstr_as_str(ent->path)); + sp_path_t parent = sp_path_parent(mem, path); + sp_fs_create_dir_at(parent); + sp_err_t err = SP_OK; switch (ent->kind) { case FS_SETUP_FILE: { - sp_fs_create_file_str(path, ent->content ? sp_str_view(ent->content) : sp_str_lit("")); + err = sp_fs_create_file_str_at(path, ent->content ? sp_cstr_as_str(ent->content) : sp_str_lit("")); break; } case FS_SETUP_DIR: { - sp_fs_create_dir(path); + err = sp_fs_create_dir_at(path); break; } - case FS_SETUP_SYMLINK: { - sp_str_t target = sp_fs_join_path(mem, sandbox, sp_str_view(ent->target)); - if (sp_fs_create_sym_link(target, path) != SP_OK) { - sp_test_fail(t, "failed to create symlink {} -> {}", sp_fmt_str(path), sp_fmt_str(target)); - } + case FS_SETUP_SYMLINK: + case FS_SETUP_DIR_SYMLINK: { + sp_fs_kind_t kind = ent->kind == FS_SETUP_DIR_SYMLINK ? SP_FS_KIND_DIR : SP_FS_KIND_FILE; + err = sp_fs_create_sym_link_at(sp_cstr_as_str(ent->target), path, kind); break; } case FS_SETUP_FIFO: { #if defined(SP_POSIX) - if (mkfifo(sp_cstr_from_str(mem, path), 0644) != 0) { - sp_test_fail(t, "failed to create fifo {}", sp_fmt_str(path)); - } + sp_str_t dir = sp_zero; + err = sp_fs_canonicalize_path_at(mem, parent, &dir); + sp_str_t fifo = sp_fs_join_path(mem, dir, sp_fs_get_name(path.sub)); + if (!err) err = mkfifo(sp_cstr_from_str(mem, fifo), 0644) ? SP_ERR_SYS : SP_OK; #else - sp_test_fail(t, "fifo setup requires posix"); + err = SP_ERR_SYS_UNSUPPORTED; #endif break; } } + if (err) sp_test_fail(t, "failed to create {}: {}", sp_fmt_cstr(ent->path), sp_fmt_str(sp_test_err_str(t, err))); } } -static void fs_expect_no_temps(sp_test_t* t, sp_str_t sandbox) { - sp_da(sp_fs_entry_t) entries = sp_zero; - sp_expect_ok(t, sp_fs_collect_recursive(sp_test_arena(t), sandbox, &entries)); - sp_da_for(entries, it) { - if (sp_str_ends_with(entries[it].name, sp_str_lit(".tmp"))) { - sp_test_fail(t, "temp file left behind: {}", sp_fmt_str(entries[it].path)); +static void fs_expect_no_temps(sp_test_t* t, sp_path_t sandbox) { + sp_fs_it_t walk = sp_fs_it_new_at(sp_test_arena(t), sandbox, 0); + while (sp_fs_it_walk(&walk)) { + if (sp_str_ends_with(walk.entry.name, sp_str_lit(".tmp"))) { + sp_test_fail(t, "temp file left behind: {}", sp_fmt_str(walk.entry.rel)); } } + sp_expect_ok(t, walk.err); + sp_fs_it_deinit(&walk); } -static void fs_expect_paths(sp_test_t* t, sp_str_t sandbox, const fs_expected_path_t expected [FS_MAX_PATHS]) { +static void fs_expect_paths(sp_test_t* t, sp_path_t sandbox, const fs_expected_path_t expected [FS_MAX_PATHS]) { sp_mem_t mem = sp_test_arena(t); sp_for(it, FS_MAX_PATHS) { const fs_expected_path_t* info = &expected[it]; if (!info->path) break; - sp_str_t path = sp_fs_join_path(mem, sandbox, sp_str_view(info->path)); + sp_str_t label = sp_cstr_as_str(info->path); + sp_path_t path = sp_path_join(mem, sandbox, label); - bool exists = sp_fs_exists(path); + bool exists = sp_fs_exists_at(path); if (exists != info->exists) { - sp_test_fail(t, "expected {} {} exist", sp_fmt_str(path), sp_fmt_cstr(info->exists ? "to" : "not to")); + sp_test_fail(t, "expected {} {} exist", sp_fmt_str(label), sp_fmt_cstr(info->exists ? "to" : "not to")); } if (info->exists) { - fs_expect_kind(t, path, sp_fs_get_kind(path), info->kind); + fs_expect_kind(t, label, sp_fs_get_kind_at(path), info->kind); } if (info->content) { sp_str_t actual = sp_zero; - sp_io_read_file(mem, path, &actual); - sp_str_t content = sp_str_view(info->content); + sp_io_read_file_at(mem, path, &actual); + sp_str_t content = sp_cstr_as_str(info->content); if (!sp_str_equal(actual, content)) { sp_test_record(t, (sp_test_failure_t) { - .message = sp_test_format(t, "content of {}", sp_fmt_str(path)), + .message = sp_test_format(t, "content of {}", sp_fmt_str(label)), .expected = sp_test_format(t, "{.quote}", sp_fmt_str(content)), .actual = sp_test_format(t, "{.quote}", sp_fmt_str(actual)), }); @@ -225,14 +182,10 @@ static void fs_expect_paths(sp_test_t* t, sp_str_t sandbox, const fs_expected_pa } if (info->target) { - sp_str_t expected = sp_fs_join_path(mem, sandbox, sp_cstr_as_str(info->target)); -#if defined(SP_WIN32) - expected = sp_str_replace_c8(mem, expected, '/', '\\'); -#endif c8 buf [SP_PATH_MAX]; sp_str_t actual = sp_zero; - sp_expect_ok(t, sp_sys_readlink_s(sp_sys_get_root(0), path, buf, sizeof(buf), &actual)); - sp_expect_str_eq(t, actual, expected); + sp_expect_ok(t, sp_sys_readlink_s(path.dir, path.sub, buf, sizeof(buf), &actual)); + sp_expect_str_eq_c(t, sp_fs_normalize_path(mem, actual), info->target); } } } diff --git a/test/fs/get_cwd.c b/test/fs/get_cwd.c index 1ab131c8..0a162274 100644 --- a/test/fs/get_cwd.c +++ b/test/fs/get_cwd.c @@ -4,31 +4,62 @@ sp_test(fs, get_cwd_contract) { sp_test_skip_on_wasm() - sp_str_t cwd = sp_fs_get_cwd(sp_test_arena(t)); + sp_str_t cwd = sp_zero; + sp_must_ok(t, sp_fs_get_cwd_path(sp_test_arena(t), &cwd)); sp_must(t, sp_fs_is_dir(cwd)); sp_must(t, sp_fs_is_absolute(cwd)); return SP_OK; } -sp_test(fs, get_cwd_unlinked_cwd_does_not_leak_deleted_suffix, .serial = true) { +sp_test(fs, get_cwd_unlinked_cwd_has_no_path, .serial = true) { // @spader #if !defined(SP_LINUX) return sp_test_skip(t, "unlinked-cwd is a Linux-only scenario"); #else sp_mem_t mem = sp_test_arena(t); - sp_str_t original = sp_fs_get_cwd(mem); - sp_must_gt(t, original.len, 0); + sp_str_t original = sp_zero; + sp_must_ok(t, sp_fs_get_cwd_path(mem, &original)); - sp_str_t sandbox = sp_test_dir(t); - sp_must_ok(t, sp_sys_chdir_s(sandbox)); - sp_must_ok(t, sp_fs_remove_dir(sandbox)); + sp_path_t sandbox = sp_test_dir(t); + sp_sys_fd_t dir = SP_SYS_INVALID_FD; + sp_must_ok(t, sp_fs_open_dir_at(sandbox, &dir)); - sp_str_t cwd = sp_fs_get_cwd(mem); + sp_path_t doomed = sp_path(dir, sp_str_lit("A")); + sp_str_t canonical = sp_zero; + sp_err_t err = sp_fs_create_dir_at(doomed); + if (!err) err = sp_fs_canonicalize_path_at(mem, doomed, &canonical); + if (!err) err = sp_sys_chdir_s(canonical); + if (!err) err = sp_fs_remove_dir_at(doomed); + + sp_str_t cwd = sp_zero; + sp_err_t unlinked = sp_fs_get_cwd_path(mem, &cwd); // restore cwd before any assertion can fail, so other tests aren't poisoned sp_sys_chdir_s(original); + sp_sys_close(dir); - sp_must(t, !sp_str_contains(cwd, sp_str_lit(" (deleted)"))); + sp_must_ok(t, err); + sp_expect_err_eq(t, unlinked, SP_ERR_SYS_NOT_FOUND); return SP_OK; #endif } + +sp_test(fs, get_cwd_is_root_zero) { + sp_test_skip_on_wasm() + sp_expect_eq(t, sp_fs_get_cwd(), sp_sys_get_root(0)); + return SP_OK; +} + +sp_test(fs, path_cwd_passes_through) { + sp_test_skip_on_wasm() + + sp_path_t file = sp_path_join(sp_test_arena(t), sp_test_dir(t), sp_str_lit("A")); + sp_must_ok(t, sp_fs_create_file_at(file)); + + sp_path_t path = sp_path_cwd(file.sub); + sp_expect_eq(t, path.dir, sp_fs_get_cwd()); + sp_expect_str_eq(t, path.sub, file.sub); + sp_expect(t, sp_fs_is_file_at(path)); + sp_expect(t, sp_fs_is_dir_at(sp_path_cwd(sp_str_lit(".")))); + return SP_OK; +} diff --git a/test/fs/get_exe_path.c b/test/fs/get_exe_path.c index 9261f02b..ea59a39d 100644 --- a/test/fs/get_exe_path.c +++ b/test/fs/get_exe_path.c @@ -4,7 +4,8 @@ sp_test(fs, get_exe_path_basic_properties) { sp_test_skip_on_wasm() - sp_str_t exe = sp_fs_get_exe_path(sp_test_arena(t)); + sp_str_t exe = sp_zero; + sp_must_ok(t, sp_fs_get_exe_path(sp_test_arena(t), &exe)); sp_must_gt(t, exe.len, 0); // normalized: no backslashes, no trailing slash @@ -19,7 +20,8 @@ sp_test(fs, get_exe_path_basic_properties) { sp_test(fs, get_exe_path_is_absolute) { sp_test_skip_on_wasm() - sp_str_t exe = sp_fs_get_exe_path(sp_test_arena(t)); + sp_str_t exe = sp_zero; + sp_must_ok(t, sp_fs_get_exe_path(sp_test_arena(t), &exe)); // absolute: starts with / on POSIX, or X: on Windows sp_must(t, (exe.data[0] == '/') || (exe.len >= 2 && exe.data[1] == ':')); return SP_OK; @@ -28,7 +30,9 @@ sp_test(fs, get_exe_path_is_absolute) { sp_test(fs, get_exe_path_exists_on_disk) { sp_test_skip_on_wasm() - sp_must(t, sp_fs_exists(sp_fs_get_exe_path(sp_test_arena(t)))); + sp_str_t exe = sp_zero; + sp_must_ok(t, sp_fs_get_exe_path(sp_test_arena(t), &exe)); + sp_must(t, sp_fs_exists(exe)); return SP_OK; } @@ -36,14 +40,19 @@ sp_test(fs, get_exe_path_is_canonical) { sp_test_skip_on_wasm() sp_mem_t mem = sp_test_arena(t); - sp_str_t exe = sp_fs_get_exe_path(mem); - sp_expect_str_eq(t, sp_fs_canonicalize_path(mem, exe), exe); + sp_str_t exe = sp_zero; + sp_str_t canonical = sp_zero; + sp_must_ok(t, sp_fs_get_exe_path(mem, &exe)); + sp_must_ok(t, sp_fs_canonicalize_path(mem, exe, &canonical)); + sp_expect_str_eq(t, canonical, exe); return SP_OK; } sp_test(fs, get_exe_path_no_dotdot) { sp_test_skip_on_wasm() - sp_must(t, !sp_str_contains(sp_fs_get_exe_path(sp_test_arena(t)), sp_str_lit(".."))); + sp_str_t exe = sp_zero; + sp_must_ok(t, sp_fs_get_exe_path(sp_test_arena(t), &exe)); + sp_must(t, !sp_str_contains(exe, sp_str_lit(".."))); return SP_OK; } diff --git a/test/fs/get_ext.c b/test/fs/get_ext.c deleted file mode 100644 index fc90332b..00000000 --- a/test/fs/get_ext.c +++ /dev/null @@ -1,28 +0,0 @@ -#include "sp.h" -#include "sp/sp_test.h" - -typedef struct { - const c8* name; - const c8* input; - const c8* expect; -} test_t; - -static const test_t tests [] = { - { .name = "empty", .input = "", .expect = "" }, - { .name = "no_ext", .input = "A", .expect = "" }, - { .name = "no_ext_with_dir", .input = "A/B", .expect = "" }, - { .name = "ext_after_dir", .input = "A/B.txt", .expect = "txt" }, - { .name = "double_dot", .input = "A..txt", .expect = "txt" }, - { .name = "leading_dot", .input = ".A", .expect = "A" }, - { .name = "simple", .input = "A.B", .expect = "B" }, - { .name = "trailing_dot", .input = "A.", .expect = "" }, - { .name = "multiple_dots", .input = "A.B.C", .expect = "C" }, - { .name = "multiple_dots_trailing", .input = "A.B.", .expect = "" }, - { .name = "windows_path", .input = "C:\\A\\B.txt", .expect = "txt" }, - { .name = "dot_in_dir", .input = "/A/B.C/D", .expect = "" }, -}; - -sp_test_each(fs, get_ext, test_t, tests) { - sp_expect_str_eq_c(t, sp_fs_get_ext(sp_str_view(it->input)), it->expect); - return SP_OK; -} diff --git a/test/fs/get_name.c b/test/fs/get_name.c index 5982f9ba..fbd5ce95 100644 --- a/test/fs/get_name.c +++ b/test/fs/get_name.c @@ -3,23 +3,34 @@ typedef struct { const c8* name; + sp_fs_path_kind_t kind; const c8* input; const c8* expect; } test_t; static const test_t tests [] = { - { .name = "empty", .input = "", .expect = "" }, - { .name = "bare", .input = "A", .expect = "A" }, - { .name = "trailing_slash", .input = "A/", .expect = "" }, - { .name = "after_dir", .input = "A/B.txt", .expect = "B.txt" }, - // backslash: get_name only splits on '/', the whole thing is the name - { .name = "backslash_not_separator", .input = "C:\\A\\B.txt", .expect = "C:\\A\\B.txt" }, - { .name = "drive_forward", .input = "C:/A/B/C.txt", .expect = "C.txt" }, - { .name = "absolute", .input = "/A", .expect = "A" }, - { .name = "absolute_trailing_slash", .input = "/A/B/", .expect = "" }, + { .name = "empty", .input = "", .expect = "" }, + { .name = "bare", .input = "A", .expect = "A" }, + { .name = "trailing_slash", .input = "A/", .expect = "A" }, + { .name = "nested", .input = "A/B.txt", .expect = "B.txt" }, + { .name = "nested_trailing", .input = "A/B/", .expect = "B" }, + { .name = "absolute", .input = "/A", .expect = "A" }, + { .name = "root", .input = "/", .expect = "" }, + { .name = "dot", .input = ".", .expect = "." }, + { .name = "dotdot", .input = "A/..", .expect = ".." }, + { .name = "backslash_is_a_name", .input = "A\\B", .expect = "A\\B" }, + { .name = "drive_is_a_name", .input = "C:/", .expect = "C:" }, + + { .name = "windows_backslash", .kind = SP_FS_PATH_WINDOWS, .input = "C:\\A\\B.txt", .expect = "B.txt" }, + { .name = "windows_mixed", .kind = SP_FS_PATH_WINDOWS, .input = "A\\B/C", .expect = "C" }, + { .name = "windows_trailing", .kind = SP_FS_PATH_WINDOWS, .input = "A\\B\\", .expect = "B" }, + { .name = "windows_dotdot", .kind = SP_FS_PATH_WINDOWS, .input = "A\\..", .expect = ".." }, + { .name = "windows_root", .kind = SP_FS_PATH_WINDOWS, .input = "\\", .expect = "" }, + { .name = "windows_drive", .kind = SP_FS_PATH_WINDOWS, .input = "C:\\", .expect = "" }, + { .name = "windows_under_drive", .kind = SP_FS_PATH_WINDOWS, .input = "C:/A", .expect = "A" }, }; sp_test_each(fs, get_name, test_t, tests) { - sp_expect_str_eq_c(t, sp_fs_get_name(sp_str_view(it->input)), it->expect); + sp_expect_str_eq_c(t, sp_fs_get_name_for(sp_cstr_as_str(it->input), it->kind), it->expect); return SP_OK; } diff --git a/test/fs/get_path_sim.c b/test/fs/get_path_sim.c new file mode 100644 index 00000000..d652e15f --- /dev/null +++ b/test/fs/get_path_sim.c @@ -0,0 +1,62 @@ +#include "sp.h" +#include "sp/sp_test.h" + +typedef enum { + PATH_CWD, + PATH_EXE, + PATH_STORAGE, + PATH_CONFIG, +} path_fn_t; + +typedef struct { + const c8* name; + path_fn_t fn; + sp_err_t err; +} test_t; + +static const test_t tests [] = { + { .name = "cwd", .fn = PATH_CWD, .err = SP_ERR_SYS_NOT_FOUND }, + { .name = "exe", .fn = PATH_EXE, .err = SP_ERR_SYS_UNSUPPORTED }, + { .name = "storage", .fn = PATH_STORAGE, .err = SP_ERR_SYS_NOT_FOUND }, + { .name = "config", .fn = PATH_CONFIG, .err = SP_ERR_SYS_NAME_TOO_LONG }, +}; + +static const test_t* active; + +static sp_err_t get_path(c8* buf, u64 size, u64* len) { + *len = 0; + return active->err; +} + +static sp_err_t get_fd_path(sp_sys_fd_t fd, c8* buf, u64 size, u64* len) { + *len = 0; + return active->err; +} + +static sp_err_t run(sp_test_t* t, test_t* c) { + sp_sys_vtable_t vt = sp_sys_vtable_platform; + vt.get_exe_path = get_path; + vt.get_storage_path = get_path; + vt.get_config_path = get_path; + vt.get_fd_path = get_fd_path; + + sp_mem_t mem = sp_test_arena(t); + sp_str_t path = sp_zero; + sp_err_t err = SP_OK; + + active = c; + const sp_sys_vtable_t* saved = sp_sys_set_vtable(&vt); + switch (c->fn) { + case PATH_CWD: err = sp_fs_get_cwd_path(mem, &path); break; + case PATH_EXE: err = sp_fs_get_exe_path(mem, &path); break; + case PATH_STORAGE: err = sp_fs_get_storage_path(mem, &path); break; + case PATH_CONFIG: err = sp_fs_get_config_path(mem, &path); break; + } + sp_sys_set_vtable(saved); + active = SP_NULLPTR; + + sp_expect_err_eq(t, err, c->err); + return SP_OK; +} + +sp_test_each_fn(fs, get_path_sim, test_t, tests, run, .serial = true); diff --git a/test/fs/get_stem.c b/test/fs/get_stem.c deleted file mode 100644 index 48d4770a..00000000 --- a/test/fs/get_stem.c +++ /dev/null @@ -1,32 +0,0 @@ -#include "sp.h" -#include "sp/sp_test.h" - -typedef struct { - const c8* name; - const c8* input; - const c8* expect; -} test_t; - -static const test_t tests [] = { - { .name = "empty", .input = "", .expect = "" }, - { .name = "bare", .input = "A", .expect = "A" }, - { .name = "trailing_slash", .input = "A/", .expect = "A/" }, - { .name = "after_dir", .input = "A/B", .expect = "B" }, - { .name = "strips_ext", .input = "A/B.txt", .expect = "B" }, - { .name = "double_dot", .input = "A..txt", .expect = "A." }, - { .name = "leading_dot", .input = ".A", .expect = "" }, - { .name = "leading_dot_with_ext", .input = ".A.txt", .expect = ".A" }, - { .name = "drive_forward", .input = "C:/A/B.txt", .expect = "B" }, - // backslash: get_name returns the whole thing, then the extension is stripped - { .name = "backslash_not_separator", .input = "C:\\A\\B.txt", .expect = "C:\\A\\B" }, - { .name = "simple", .input = "A.B", .expect = "A" }, - { .name = "trailing_dot", .input = "A.", .expect = "A" }, - { .name = "multiple_dots", .input = "A.B.C", .expect = "A.B" }, - { .name = "multiple_dots_trailing", .input = "A.B.", .expect = "A.B" }, - { .name = "hidden_single", .input = ".B", .expect = "" }, -}; - -sp_test_each(fs, get_stem, test_t, tests) { - sp_expect_str_eq_c(t, sp_fs_get_stem(sp_str_view(it->input)), it->expect); - return SP_OK; -} diff --git a/test/fs/host_kind.c b/test/fs/host_kind.c new file mode 100644 index 00000000..e4abf384 --- /dev/null +++ b/test/fs/host_kind.c @@ -0,0 +1,58 @@ +#include "sp.h" +#include "sp/sp_test.h" + +typedef enum { + FN_IS_SEP, + FN_IS_ROOT, + FN_IS_ABSOLUTE, + FN_TRIM_PATH, + FN_GET_NAME, + FN_PARENT_PATH, + FN_GET_EXT, + FN_GET_STEM, + FN_REPLACE_EXT, + FN_JOIN_PATH, + FN_NORMALIZE_PATH, +} fn_t; + +typedef struct { + const c8* name; + fn_t fn; + const c8* input; +} test_t; + +static const test_t tests [] = { + { .name = "is_sep", .fn = FN_IS_SEP, .input = "\\" }, + { .name = "is_root", .fn = FN_IS_ROOT, .input = "C:\\" }, + { .name = "is_absolute", .fn = FN_IS_ABSOLUTE, .input = "\\A" }, + { .name = "trim_path", .fn = FN_TRIM_PATH, .input = "A\\" }, + { .name = "get_name", .fn = FN_GET_NAME, .input = "A\\B" }, + { .name = "parent_path", .fn = FN_PARENT_PATH, .input = "A\\B" }, + { .name = "get_ext", .fn = FN_GET_EXT, .input = "A.B\\C" }, + { .name = "get_stem", .fn = FN_GET_STEM, .input = "A.B\\C" }, + { .name = "replace_ext", .fn = FN_REPLACE_EXT, .input = "A.B\\C" }, + { .name = "join_path", .fn = FN_JOIN_PATH, .input = "A\\" }, + { .name = "normalize_path", .fn = FN_NORMALIZE_PATH, .input = "A\\B" }, +}; + +sp_test_each(fs, host_kind, test_t, tests) { + sp_mem_t mem = sp_test_arena(t); + sp_fs_path_kind_t kind = sp_os_get_path_kind(); + sp_str_t path = sp_cstr_as_str(it->input); + sp_str_t other = sp_str_lit("D"); + + switch (it->fn) { + case FN_IS_SEP: sp_expect_eq(t, sp_fs_is_sep(path.data[0]), sp_fs_is_sep_for(path.data[0], kind)); break; + case FN_IS_ROOT: sp_expect_eq(t, sp_fs_is_root(path), sp_fs_is_root_for(path, kind)); break; + case FN_IS_ABSOLUTE: sp_expect_eq(t, sp_fs_is_absolute(path), sp_fs_is_absolute_for(path, kind)); break; + case FN_TRIM_PATH: sp_expect_str_eq(t, sp_fs_trim_path(path), sp_fs_trim_path_for(path, kind)); break; + case FN_GET_NAME: sp_expect_str_eq(t, sp_fs_get_name(path), sp_fs_get_name_for(path, kind)); break; + case FN_PARENT_PATH: sp_expect_str_eq(t, sp_fs_parent_path(path), sp_fs_parent_path_for(path, kind)); break; + case FN_GET_EXT: sp_expect_str_eq(t, sp_fs_get_ext(path), sp_fs_get_ext_for(path, kind)); break; + case FN_GET_STEM: sp_expect_str_eq(t, sp_fs_get_stem(path), sp_fs_get_stem_for(path, kind)); break; + case FN_REPLACE_EXT: sp_expect_str_eq(t, sp_fs_replace_ext(mem, path, other), sp_fs_replace_ext_for(mem, path, other, kind)); break; + case FN_JOIN_PATH: sp_expect_str_eq(t, sp_fs_join_path(mem, path, other), sp_fs_join_path_for(mem, path, other, kind)); break; + case FN_NORMALIZE_PATH: sp_expect_str_eq(t, sp_fs_normalize_path(mem, path), sp_fs_normalize_path_for(mem, path, kind)); break; + } + return SP_OK; +} diff --git a/test/fs/is_absolute.c b/test/fs/is_absolute.c index 438d83df..1fbdd3b3 100644 --- a/test/fs/is_absolute.c +++ b/test/fs/is_absolute.c @@ -11,12 +11,13 @@ typedef struct { static const test_t tests [] = { { .name = "empty", .input = "" }, { .name = "root_slash", .input = "/", .posix = true, .windows = true }, - { .name = "root_backslash", .input = "\\", .posix = true, .windows = true }, + { .name = "root_backslash", .input = "\\", .windows = true }, { .name = "double_slash", .input = "//", .posix = true, .windows = true }, { .name = "slash_prefix", .input = "/A", .posix = true, .windows = true }, - { .name = "backslash_prefix", .input = "\\A", .posix = true, .windows = true }, + { .name = "backslash_prefix", .input = "\\A", .windows = true }, { .name = "relative", .input = "A" }, { .name = "relative_nested", .input = "A/B" }, + { .name = "dot", .input = "." }, { .name = "drive_bare", .input = "C:" }, { .name = "drive_lower_bare", .input = "a:" }, { .name = "drive_relative", .input = "C:A" }, @@ -27,7 +28,7 @@ static const test_t tests [] = { }; sp_test_each(fs, is_absolute, test_t, tests) { - sp_str_t path = sp_str_view(it->input); + sp_str_t path = sp_cstr_as_str(it->input); sp_expect_eq(t, sp_fs_is_absolute_for(path, SP_FS_PATH_POSIX), it->posix); sp_expect_eq(t, sp_fs_is_absolute_for(path, SP_FS_PATH_WINDOWS), it->windows); return SP_OK; diff --git a/test/fs/is_glob.c b/test/fs/is_glob.c deleted file mode 100644 index 10a62ea2..00000000 --- a/test/fs/is_glob.c +++ /dev/null @@ -1,24 +0,0 @@ -#include "sp.h" -#include "sp/sp_test.h" - -typedef struct { - const c8* name; - const c8* input; - bool glob; -} test_t; - -static const test_t tests [] = { - { .name = "empty", .input = "" }, - { .name = "plain", .input = "A" }, - { .name = "plain_ext", .input = "A.txt" }, - { .name = "question_not_glob", .input = "A?B" }, - { .name = "star", .input = "*", .glob = true }, - { .name = "star_ext", .input = "*.txt", .glob = true }, - { .name = "star_in_dir", .input = "A/*", .glob = true }, - { .name = "star_infix", .input = "A*B", .glob = true }, -}; - -sp_test_each(fs, is_glob, test_t, tests) { - sp_expect_eq(t, sp_fs_is_glob(sp_str_view(it->input)), it->glob); - return SP_OK; -} diff --git a/test/fs/is_root.c b/test/fs/is_root.c index bdff5004..93a07ae6 100644 --- a/test/fs/is_root.c +++ b/test/fs/is_root.c @@ -4,24 +4,30 @@ typedef struct { const c8* name; const c8* input; - bool root; + bool posix; + bool windows; } test_t; static const test_t tests [] = { - { .name = "empty", .input = "" }, - { .name = "slash", .input = "/", .root = true }, - { .name = "backslash", .input = "\\", .root = true }, - { .name = "drive_bare", .input = "C:" }, - { .name = "drive_lower_bare", .input = "a:" }, - { .name = "drive_slash", .input = "C:/", .root = true }, - { .name = "drive_backslash", .input = "C:\\", .root = true }, - { .name = "relative", .input = "A" }, - { .name = "slash_prefix", .input = "/A" }, - { .name = "drive_prefix", .input = "C:/A" }, - { .name = "double_slash", .input = "//" }, + { .name = "empty", .input = "" }, + { .name = "slash", .input = "/", .posix = true, .windows = true }, + { .name = "slash_run", .input = "//", .posix = true, .windows = true }, + { .name = "backslash", .input = "\\", .windows = true }, + { .name = "drive_bare", .input = "C:" }, + { .name = "drive_lower_bare", .input = "a:" }, + { .name = "drive_slash", .input = "C:/", .windows = true }, + { .name = "drive_slash_run", .input = "C://", .windows = true }, + { .name = "drive_backslash", .input = "C:\\", .windows = true }, + { .name = "relative", .input = "A" }, + { .name = "dot", .input = "." }, + { .name = "slash_prefix", .input = "/A" }, + { .name = "slash_prefix_trailing", .input = "/A/" }, + { .name = "drive_prefix", .input = "C:/A" }, }; sp_test_each(fs, is_root, test_t, tests) { - sp_expect_eq(t, sp_fs_is_root(sp_str_view(it->input)), it->root); + sp_str_t path = sp_cstr_as_str(it->input); + sp_expect_eq(t, sp_fs_is_root_for(path, SP_FS_PATH_POSIX), it->posix); + sp_expect_eq(t, sp_fs_is_root_for(path, SP_FS_PATH_WINDOWS), it->windows); return SP_OK; } diff --git a/test/fs/is_sep.c b/test/fs/is_sep.c new file mode 100644 index 00000000..e857f6fb --- /dev/null +++ b/test/fs/is_sep.c @@ -0,0 +1,21 @@ +#include "sp.h" +#include "sp/sp_test.h" + +typedef struct { + const c8* name; + c8 input; + bool posix; + bool windows; +} test_t; + +static const test_t tests [] = { + { .name = "slash", .input = '/', .posix = true, .windows = true }, + { .name = "backslash", .input = '\\', .windows = true }, + { .name = "letter", .input = 'A' }, +}; + +sp_test_each(fs, is_sep, test_t, tests) { + sp_expect_eq(t, sp_fs_is_sep_for(it->input, SP_FS_PATH_POSIX), it->posix); + sp_expect_eq(t, sp_fs_is_sep_for(it->input, SP_FS_PATH_WINDOWS), it->windows); + return SP_OK; +} diff --git a/test/fs/it/enter.c b/test/fs/it/enter.c new file mode 100644 index 00000000..4bee6d62 --- /dev/null +++ b/test/fs/it/enter.c @@ -0,0 +1,165 @@ +#include "fs.h" + +typedef struct { + const c8* path; + sp_err_t err; + bool vanish; +} enter_t; + +typedef struct { + const c8* path; + sp_fs_kind_t kind; +} entry_t; + +typedef struct { + entry_t entries [FS_MAX_PATHS]; +} expect_t; + +typedef struct { + const c8* name; + fs_setup_t setup [FS_MAX_SETUP]; + enter_t enter [FS_MAX_PATHS]; + expect_t expect; +} test_t; + +static const test_t tests [] = { + { + .name = "enters_only_listed", + .setup = { + { "R/A/B" }, + { "R/C/D" }, + { "R/E" }, + }, + .enter = { { "A" } }, + .expect = { + .entries = { + { "A", SP_FS_KIND_DIR }, + { "A/B", SP_FS_KIND_FILE }, + { "C", SP_FS_KIND_DIR }, + { "E", SP_FS_KIND_FILE }, + }, + }, + }, + { + .name = "nested_enters", + .setup = { + { "R/A/B/C" }, + }, + .enter = { { "A" }, { "A/B" } }, + .expect = { + .entries = { + { "A", SP_FS_KIND_DIR }, + { "A/B", SP_FS_KIND_DIR }, + { "A/B/C", SP_FS_KIND_FILE }, + }, + }, + }, + { + .name = "enter_file_is_not_dir", + .setup = { + { "R/A" }, + { "R/B" }, + }, + .enter = { { "A", SP_ERR_SYS_NOT_DIR } }, + .expect = { + .entries = { + { "A", SP_FS_KIND_FILE }, + { "B", SP_FS_KIND_FILE }, + }, + }, + }, + { + .name = "enter_symlink_is_loop", + .setup = { + { "R/A", FS_SETUP_DIR }, + { "R/A/B" }, + { .path = "R/L", .kind = FS_SETUP_DIR_SYMLINK, .target = "A" }, + }, + .enter = { { "L", SP_ERR_SYS_LOOP } }, + .expect = { + .entries = { + { "A", SP_FS_KIND_DIR }, + { "L", SP_FS_KIND_SYMLINK }, + }, + }, + }, + { + .name = "enter_vanished_is_not_found", + .setup = { + { "R/A", FS_SETUP_DIR }, + { "R/C" }, + }, + .enter = { { "A", SP_ERR_SYS_NOT_FOUND, .vanish = true } }, + .expect = { + .entries = { + { "A", SP_FS_KIND_DIR }, + { "C", SP_FS_KIND_FILE }, + }, + }, + }, +}; + +static const enter_t* find_enter(sp_mem_t mem, const test_t* test, sp_str_t base, sp_str_t path) { + sp_carr_for(test->enter, e) { + const enter_t* enter = &test->enter[e]; + if (!enter->path) break; + if (sp_str_equal(sp_fs_join_path(mem, base, sp_cstr_as_str(enter->path)), path)) return enter; + } + return SP_NULLPTR; +} + +sp_test_each(fs, it_enter, test_t, tests) { + if (fs_setup_needs_symlinks(it->setup)) sp_test_skip_without_symlinks(); + + sp_mem_t mem = sp_test_arena(t); + sp_path_t sandbox = sp_test_dir(t); + fs_apply_setup(t, sandbox, it->setup); + + sp_path_t root = sp_path_join(mem, sandbox, sp_str_lit("R")); + sp_str_t base = root.sub; + fs_match_t entries [FS_MAX_PATHS] = sp_zero; + u32 num_entries = 0; + sp_carr_for(it->expect.entries, e) { + const entry_t* want = &it->expect.entries[e]; + if (!want->path) break; + entries[num_entries++] = (fs_match_t) { .key = sp_fs_join_path(mem, base, sp_cstr_as_str(want->path)), .kind = want->kind }; + } + + fs_match_t leaves [FS_MAX_PATHS] = sp_zero; + u32 num_leaves = 0; + sp_str_t entered [FS_MAX_PATHS] = sp_zero; + u32 depth = 0; + + sp_fs_it_t walk = sp_fs_it_new_at(mem, root, 0); + while (sp_fs_it_next(&walk)) { + sp_expect_str_eq(t, sp_fs_join_path(mem, base, walk.entry.rel), walk.entry.path); + switch (walk.yield) { + case SP_FS_IT_LEAVE: { + fs_match(t, leaves, num_leaves, walk.entry.path, walk.entry.kind); + sp_expect_eq(t, (u32)sp_fs_get_kind_at(walk.at), (u32)SP_FS_KIND_DIR); + sp_must(t, depth > 0); + sp_expect_str_eq(t, walk.entry.path, entered[--depth]); + break; + } + case SP_FS_IT_ENTRY: { + fs_match(t, entries, num_entries, walk.entry.path, walk.entry.kind); + const enter_t* enter = find_enter(mem, it, base, walk.entry.path); + if (!enter) break; + if (enter->vanish) sp_must_ok(t, sp_sys_rmdir_s(walk.at.dir, walk.at.sub)); + sp_expect_err_eq(t, sp_fs_it_enter(&walk), enter->err); + if (enter->err) break; + sp_str_t path = sp_str_copy(mem, walk.entry.path); + entered[depth++] = path; + leaves[num_leaves++] = (fs_match_t) { .key = path, .kind = SP_FS_KIND_DIR }; + break; + } + } + } + sp_expect_ok(t, walk.err); + sp_expect_eq(t, depth, (u32)0); + sp_fs_it_deinit(&walk); + + fs_match_finish(t, entries, num_entries); + fs_match_finish(t, leaves, num_leaves); + return SP_OK; +} diff --git a/test/fs/it/enter_sim.c b/test/fs/it/enter_sim.c new file mode 100644 index 00000000..d9ea4ef9 --- /dev/null +++ b/test/fs/it/enter_sim.c @@ -0,0 +1,175 @@ +#include "sp.h" +#include "sp/sp_test.h" +#include "sim.h" + +#define MAX_YIELDS 6 + +typedef struct { + const c8* path; + sp_err_t err; + sp_fs_it_yield_t yield; +} enter_t; + +typedef struct { + const c8* path; + const c8* name; + sp_fs_kind_t kind; + sp_fs_it_yield_t yield; +} yield_t; + +typedef struct { + yield_t yields [MAX_YIELDS]; + u32 opens; +} expect_t; + +typedef struct { + const c8* name; + sim_dir_t dirs [SIM_MAX_DIRS]; + enter_t enter [MAX_YIELDS]; + expect_t expect; +} test_t; + +static const test_t tests [] = { + { + .name = "leaves_after_children_before_siblings", + .dirs = { + { .path = "T", .entries = { { "B", SP_FS_KIND_DIR }, { "C", SP_FS_KIND_FILE } } }, + { .path = "T/B", .entries = { { "D", SP_FS_KIND_FILE } } }, + }, + .enter = { { "T/B" } }, + .expect = { + .yields = { + { "T/B", "B", SP_FS_KIND_DIR }, + { "T/B/D", "D", SP_FS_KIND_FILE }, + { "T/B", "B", SP_FS_KIND_DIR, SP_FS_IT_LEAVE }, + { "T/C", "C", SP_FS_KIND_FILE }, + }, + .opens = 2, + }, + }, + { + .name = "pruned_dir_is_never_opened", + .dirs = { + { .path = "T", .entries = { { "B", SP_FS_KIND_DIR } } }, + { .path = "T/B", .entries = { { "C", SP_FS_KIND_FILE } } }, + }, + .expect = { + .yields = { + { "T/B", "B", SP_FS_KIND_DIR }, + }, + .opens = 1, + }, + }, + { + .name = "enter_ignores_kind_hint", + .dirs = { + { .path = "T", .entries = { { "B", SP_FS_KIND_FILE } } }, + { .path = "T/B", .entries = { { "C", SP_FS_KIND_FILE } } }, + }, + .enter = { { "T/B" } }, + .expect = { + .yields = { + { "T/B", "B", SP_FS_KIND_FILE }, + { "T/B/C", "C", SP_FS_KIND_FILE }, + { "T/B", "B", SP_FS_KIND_DIR, SP_FS_IT_LEAVE }, + }, + .opens = 2, + }, + }, + { + .name = "enter_error_is_returned_and_walk_continues", + .dirs = { + { .path = "T", .entries = { { "B", SP_FS_KIND_DIR }, { "C", SP_FS_KIND_FILE } } }, + { .path = "T/B", .open = SP_ERR_SYS_IO }, + }, + .enter = { { "T/B", SP_ERR_SYS_IO } }, + .expect = { + .yields = { + { "T/B", "B", SP_FS_KIND_DIR }, + { "T/C", "C", SP_FS_KIND_FILE }, + }, + .opens = 2, + }, + }, + { + .name = "enter_at_leave_relists", + .dirs = { + { .path = "T", .entries = { { "B", SP_FS_KIND_DIR } } }, + { .path = "T/B", .entries = { { "D", SP_FS_KIND_FILE } } }, + }, + .enter = { { "T/B" }, { "T/B", .yield = SP_FS_IT_LEAVE } }, + .expect = { + .yields = { + { "T/B", "B", SP_FS_KIND_DIR }, + { "T/B/D", "D", SP_FS_KIND_FILE }, + { "T/B", "B", SP_FS_KIND_DIR, SP_FS_IT_LEAVE }, + { "T/B/D", "D", SP_FS_KIND_FILE }, + { "T/B", "B", SP_FS_KIND_DIR, SP_FS_IT_LEAVE }, + }, + .opens = 3, + }, + }, +}; + +static s32 find_enter(const test_t* test, sp_str_t path, sp_fs_it_yield_t yield, const bool* used) { + sp_carr_for(test->enter, e) { + const enter_t* enter = &test->enter[e]; + if (!enter->path) break; + if (used[e] || enter->yield != yield) continue; + if (sp_str_equal(sp_cstr_as_str(enter->path), path)) return (s32)e; + } + return -1; +} + +sp_test_each(fs, it_enter_sim, test_t, tests, .serial = true) { + sp_mem_t mem = sp_test_arena(t); + + sim_t s = sp_zero; + sim_begin(&s, it->dirs); + + u32 leaves = 0; + u32 produced = 0; + bool used [MAX_YIELDS] = sp_zero; + sp_fs_it_t walk = sp_fs_it_new_at(mem, sp_path(sp_sys_get_root(0), sp_str_lit("T")), 0); + while (sp_fs_it_next(&walk)) { + if (produced < MAX_YIELDS && it->expect.yields[produced].path) { + const yield_t* want = &it->expect.yields[produced]; + sp_expect_str_eq_c(t, walk.entry.path, want->path); + sp_expect_str_eq_c(t, walk.entry.name, want->name); + sp_expect_str_eq(t, walk.at.sub, walk.entry.name); + sp_expect_str_eq(t, sp_fs_join_path(mem, sp_str_lit("T"), walk.entry.rel), walk.entry.path); + sp_expect_eq(t, (u32)walk.entry.kind, (u32)want->kind); + sp_expect_eq(t, (u32)walk.yield, (u32)want->yield); + } + else { + sp_test_fail(t, "walker produced unexpected yield {}", sp_fmt_str(walk.entry.path)); + } + produced++; + + if (walk.yield == SP_FS_IT_LEAVE) { + leaves++; + sp_expect_eq(t, s.count.closes, leaves); + } + + s32 enter = find_enter(it, walk.entry.path, walk.yield, used); + if (enter >= 0) { + used[enter] = true; + sp_expect_err_eq(t, sp_fs_it_enter(&walk), it->enter[enter].err); + } + } + sp_expect_ok(t, walk.err); + sp_fs_it_deinit(&walk); + + sim_end(&s); + + u32 expected = 0; + sp_carr_for(it->expect.yields, e) { + if (!it->expect.yields[e].path) break; + expected++; + } + sp_expect_eq(t, produced, expected); + sp_expect_eq(t, s.count.opens, it->expect.opens); + sp_expect_eq(t, s.count.nofollow, s.count.opens - 1); + sp_expect_eq(t, s.count.closes, s.count.dirs); + return SP_OK; +} diff --git a/test/fs/it/os.c b/test/fs/it/os.c index 9f149aa1..fae60093 100644 --- a/test/fs/it/os.c +++ b/test/fs/it/os.c @@ -105,7 +105,7 @@ static const test_t tests [] = { .setup = { { "R/A", FS_SETUP_DIR }, { "R/A/B" }, - { .path = "R/L", .kind = FS_SETUP_SYMLINK, .target = "R/A" }, + { .path = "R/L", .kind = FS_SETUP_DIR_SYMLINK, .target = "A" }, }, .recursive = true, .expect = { @@ -161,14 +161,17 @@ static const test_t tests [] = { }; sp_test_each(fs, it, test_t, tests) { - skip_if_symlinks_needed(t, it->setup); + if (fs_setup_needs_symlinks(it->setup)) sp_test_skip_without_symlinks(); sp_mem_t mem = sp_test_arena(t); - sp_str_t sandbox = sp_test_dir(t); + sp_path_t sandbox = sp_test_dir(t); fs_apply_setup(t, sandbox, it->setup); - sp_str_t base = sp_fs_join_path(mem, sandbox, sp_str_lit("R")); - sp_str_t root = sp_fmt(mem, "{}/{}", sp_fmt_str(sandbox), sp_fmt_cstr(it->root ? it->root : "R")).value; + sp_sys_fd_t dir = SP_SYS_INVALID_FD; + sp_must_ok(t, sp_fs_open_dir_at(sandbox, &dir)); + + sp_str_t base = sp_str_lit("R"); + sp_path_t root = sp_path(dir, sp_cstr_as_str(it->root ? it->root : "R")); fs_match_t matches [FS_MAX_PATHS] = sp_zero; u32 n = 0; @@ -178,15 +181,19 @@ sp_test_each(fs, it, test_t, tests) { matches[n++] = (fs_match_t) { .key = sp_fs_join_path(mem, base, sp_cstr_as_str(want->path)), .kind = want->kind }; } - sp_fs_it_t walk = it->recursive ? sp_fs_it_new_recursive(mem, root) : sp_fs_it_new(mem, root); - while (sp_fs_it_next(&walk)) { + sp_fs_it_t walk = sp_fs_it_new_at(mem, root, 0); + while (it->recursive ? sp_fs_it_walk(&walk) : sp_fs_it_next(&walk)) { sp_expect_str_eq(t, walk.entry.name, sp_fs_get_name(walk.entry.path)); + sp_expect_str_eq(t, sp_fs_join_path(mem, base, walk.entry.rel), walk.entry.path); + sp_expect_str_eq(t, walk.at.sub, walk.entry.name); + sp_expect_eq(t, (u32)sp_fs_get_kind_at(walk.at), (u32)walk.entry.kind); fs_match(t, matches, n, walk.entry.path, walk.entry.kind); } sp_expect_err_eq(t, walk.err, it->expect.err); sp_expect(t, !sp_fs_it_next(&walk)); sp_expect_err_eq(t, walk.err, it->expect.err); sp_fs_it_deinit(&walk); + sp_sys_close(dir); fs_match_finish(t, matches, n); return SP_OK; diff --git a/test/fs/it/sim.c b/test/fs/it/sim.c index 74aba99e..8ba0377f 100644 --- a/test/fs/it/sim.c +++ b/test/fs/it/sim.c @@ -20,6 +20,7 @@ typedef struct { typedef struct { sp_err_t open; sp_err_t walk; + const c8* failed; entry_t entries [MAX_WALK]; u32 opens; } expect_t; @@ -27,6 +28,7 @@ typedef struct { typedef struct { const c8* name; const c8* root; + u32 flags; bool recursive; u32 stop_after; sim_dir_t dirs [SIM_MAX_DIRS]; @@ -84,6 +86,7 @@ static const test_t tests [] = { }, .expect = { .walk = SP_ERR_SYS_NOT_FOUND, + .failed = "T/B", .opens = 2, }, }, @@ -115,6 +118,17 @@ static const test_t tests [] = { .opens = 2, }, }, + { + .name = "root_flags_forwarded", + .root = "T", + .flags = SP_SYS_OPEN_DIR_NO_FOLLOW, + .dirs = { + { .path = "T" }, + }, + .expect = { + .opens = 1, + }, + }, { .name = "path_fits_max", .root = "T", @@ -178,16 +192,17 @@ sp_test_each(fs, it_sim, test_t, tests, .serial = true) { sim_t s = sp_zero; sim_begin(&s, it->dirs); - sp_fs_it_t walk = it->recursive ? sp_fs_it_new_recursive(mem, root) : sp_fs_it_new(mem, root); + sp_fs_it_t walk = sp_fs_it_new_at(mem, sp_path(sp_sys_get_root(0), root), it->flags); sp_expect_err_eq(t, walk.err, it->expect.open); sp_expect(t, !walk.entry.path.data); u32 produced = 0; - while (sp_fs_it_next(&walk)) { + while (it->recursive ? sp_fs_it_walk(&walk) : sp_fs_it_next(&walk)) { if (produced < MAX_WALK && it->expect.entries[produced].path) { const entry_t* want = &it->expect.entries[produced]; sp_expect_str_eq_c(t, walk.entry.path, want->path); sp_expect_str_eq_c(t, walk.entry.name, want->name); + sp_expect_str_eq(t, sp_fs_join_path(mem, root, walk.entry.rel), walk.entry.path); sp_expect_eq(t, (u32)walk.entry.kind, (u32)want->kind); } else { @@ -197,6 +212,7 @@ sp_test_each(fs, it_sim, test_t, tests, .serial = true) { if (produced == it->stop_after) break; } sp_expect_err_eq(t, walk.err, it->expect.walk); + if (it->expect.failed) sp_expect_str_eq_c(t, walk.entry.path, it->expect.failed); sp_fs_it_deinit(&walk); sim_end(&s); @@ -209,5 +225,7 @@ sp_test_each(fs, it_sim, test_t, tests, .serial = true) { sp_expect_eq(t, produced, expected); sp_expect_eq(t, s.count.opens, it->expect.opens); sp_expect_eq(t, s.count.closes, s.count.dirs); + u32 root_nofollow = (it->flags & SP_SYS_OPEN_DIR_NO_FOLLOW) ? 1 : 0; + if (s.count.opens) sp_expect_eq(t, s.count.nofollow, s.count.opens - 1 + root_nofollow); return SP_OK; } diff --git a/test/fs/join_path.c b/test/fs/join_path.c index 60b41328..118b3b40 100644 --- a/test/fs/join_path.c +++ b/test/fs/join_path.c @@ -3,27 +3,38 @@ typedef struct { const c8* name; + sp_fs_path_kind_t kind; const c8* a; const c8* b; const c8* expect; } test_t; static const test_t tests [] = { - { .name = "basic", .a = "A", .b = "B", .expect = "A/B" }, - { .name = "trailing_slash_lhs", .a = "A/", .b = "B", .expect = "A/B" }, - { .name = "trailing_slash_both", .a = "A/", .b = "B/", .expect = "A/B" }, - { .name = "nested_rhs", .a = "A", .b = "B/C", .expect = "A/B/C" }, - { .name = "root_lhs", .a = "/", .b = "A", .expect = "/A" }, - { .name = "root_lhs_nested", .a = "/", .b = "A/B", .expect = "/A/B" }, - { .name = "drive_slash_lhs", .a = "C:/", .b = "A", .expect = "C:/A" }, - { .name = "drive_bare_lhs", .a = "C:", .b = "A", .expect = "C:/A" }, - { .name = "empty_lhs", .a = "", .b = "A", .expect = "A" }, - { .name = "empty_rhs", .a = "A", .b = "", .expect = "A" }, - { .name = "empty_both", .a = "", .b = "", .expect = "" }, + { .name = "basic", .a = "A", .b = "B", .expect = "A/B" }, + { .name = "nested_rhs", .a = "A", .b = "B/C", .expect = "A/B/C" }, + { .name = "trailing_lhs", .a = "A/", .b = "B", .expect = "A/B" }, + { .name = "trailing_rhs", .a = "A", .b = "B/", .expect = "A/B" }, + { .name = "leading_rhs", .a = "A", .b = "/B", .expect = "A/B" }, + { .name = "leading_run_rhs", .a = "A/", .b = "//B", .expect = "A/B" }, + { .name = "absolute_both", .a = "/A", .b = "/B", .expect = "/A/B" }, + { .name = "root_lhs", .a = "/", .b = "A", .expect = "/A" }, + { .name = "root_both", .a = "/", .b = "/A", .expect = "/A" }, + { .name = "root_rhs", .a = "A", .b = "/", .expect = "A" }, + { .name = "dot_lhs", .a = ".", .b = "A", .expect = "./A" }, + { .name = "empty_lhs", .a = "", .b = "A", .expect = "A" }, + { .name = "empty_lhs_absolute", .a = "", .b = "/A", .expect = "/A" }, + { .name = "empty_rhs", .a = "A", .b = "", .expect = "A" }, + { .name = "empty_both", .a = "", .b = "", .expect = "" }, + { .name = "backslash_is_a_name", .a = "A\\", .b = "\\B", .expect = "A\\/\\B" }, + + { .name = "windows_trailing_lhs", .kind = SP_FS_PATH_WINDOWS, .a = "A\\", .b = "B", .expect = "A/B" }, + { .name = "windows_leading_rhs", .kind = SP_FS_PATH_WINDOWS, .a = "A", .b = "\\B", .expect = "A/B" }, + { .name = "windows_drive", .kind = SP_FS_PATH_WINDOWS, .a = "C:/", .b = "A", .expect = "C:/A" }, + { .name = "windows_drive_back", .kind = SP_FS_PATH_WINDOWS, .a = "C:\\", .b = "A", .expect = "C:\\A" }, }; sp_test_each(fs, join_path, test_t, tests) { - sp_str_t result = sp_fs_join_path(sp_test_arena(t), sp_str_view(it->a), sp_str_view(it->b)); + sp_str_t result = sp_fs_join_path_for(sp_test_arena(t), sp_cstr_as_str(it->a), sp_cstr_as_str(it->b), it->kind); sp_expect_str_eq_c(t, result, it->expect); return SP_OK; } diff --git a/test/fs/links.c b/test/fs/links.c index 792128cb..094189d8 100644 --- a/test/fs/links.c +++ b/test/fs/links.c @@ -1,23 +1,28 @@ #include "fs.h" typedef struct { + const c8* name; + fs_setup_t setup [FS_MAX_SETUP]; + const c8* target; + const c8* link; + const c8* rewrite; sp_err_t err; - fs_expected_path_t paths [FS_MAX_PATHS]; -} expect_t; + fs_expected_path_t expect [FS_MAX_PATHS]; +} hard_t; typedef struct { const c8* name; fs_setup_t setup [FS_MAX_SETUP]; - bool symlink; const c8* target; + sp_fs_kind_t kind; const c8* link; - const c8* rewrite; - expect_t expect; -} test_t; + sp_err_t err; + fs_expected_path_t expect [FS_MAX_PATHS]; +} sym_t; -static const test_t tests [] = { +static const hard_t hard_links [] = { { - .name = "hard_link_file", + .name = "file", .setup = { { .path = "A", .content = "A" }, }, @@ -25,122 +30,131 @@ static const test_t tests [] = { .link = "B", .rewrite = "B", .expect = { - .paths = { - { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE }, - { .path = "B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "B" }, - }, + { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE }, + { .path = "B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "B" }, }, }, { - .name = "hard_link_existing_destination_fails", + .name = "existing_destination_fails", .setup = { { .path = "A", .content = "A" }, { .path = "B", .content = "B" }, }, .target = "A", .link = "B", + .err = SP_ERR_SYS_EXISTS, .expect = { - .err = SP_ERR_SYS_EXISTS, - .paths = { - { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE }, - { .path = "B", .exists = true, .kind = SP_FS_KIND_FILE }, - }, + { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE }, + { .path = "B", .exists = true, .kind = SP_FS_KIND_FILE }, }, }, { - .name = "hard_link_directory_fails", + .name = "directory_fails", .setup = { { "A", FS_SETUP_DIR }, }, .target = "A", .link = "B", - .expect = { - // hard-linking a directory: POSIX reports EPERM; NT reports - // STATUS_FILE_IS_A_DIRECTORY from FileLinkInformation + // hard-linking a directory: POSIX reports EPERM; NT reports + // STATUS_FILE_IS_A_DIRECTORY from FileLinkInformation #if defined(SP_WIN32) - .err = SP_ERR_SYS_IS_DIR, + .err = SP_ERR_SYS_IS_DIR, #else - .err = SP_ERR_SYS_ACCESS_DENIED, + .err = SP_ERR_SYS_ACCESS_DENIED, #endif - .paths = { - { .path = "A", .exists = true, .kind = SP_FS_KIND_DIR }, - { .path = "B" }, - }, + .expect = { + { .path = "A", .exists = true, .kind = SP_FS_KIND_DIR }, + { .path = "B" }, }, }, +}; + +static const sym_t sym_links [] = { { - .name = "symlink_file", + .name = "file", .setup = { { .path = "A", .content = "A" }, }, - .symlink = true, .target = "A", + .kind = SP_FS_KIND_FILE, .link = "L", .expect = { - .paths = { - { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE }, - { .path = "L", .exists = true, .kind = SP_FS_KIND_SYMLINK, .content = "A" }, - }, + { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE }, + { .path = "L", .exists = true, .kind = SP_FS_KIND_SYMLINK, .content = "A", .target = "A" }, }, }, { - .name = "symlink_directory", + .name = "directory", .setup = { - { "A", FS_SETUP_DIR }, + { .path = "A/F", .content = "F" }, }, - .symlink = true, .target = "A", + .kind = SP_FS_KIND_DIR, .link = "L", .expect = { - .paths = { - { .path = "A", .exists = true, .kind = SP_FS_KIND_DIR }, - { .path = "L", .exists = true, .kind = SP_FS_KIND_SYMLINK }, - }, + { .path = "A", .exists = true, .kind = SP_FS_KIND_DIR }, + { .path = "L", .exists = true, .kind = SP_FS_KIND_SYMLINK, .target = "A" }, + { .path = "L/F", .exists = true, .kind = SP_FS_KIND_FILE, .content = "F" }, }, }, { - .name = "symlink_existing_destination_fails", + .name = "dangling", + .target = "A", + .kind = SP_FS_KIND_FILE, + .link = "L", + .expect = { + { .path = "A" }, + { .path = "L", .target = "A" }, + }, + }, + { + .name = "existing_destination_fails", .setup = { { .path = "A", .content = "A" }, { .path = "B", .content = "B" }, }, - .symlink = true, .target = "A", + .kind = SP_FS_KIND_FILE, .link = "B", + .err = SP_ERR_SYS_EXISTS, .expect = { - .err = SP_ERR_SYS_EXISTS, - .paths = { - { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE }, - { .path = "B", .exists = true, .kind = SP_FS_KIND_FILE }, - }, + { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE }, + { .path = "B", .exists = true, .kind = SP_FS_KIND_FILE }, }, }, }; -sp_test_each(fs, links, test_t, tests) { - if (it->symlink) fs_skip_if_no_symlinks(t); - +sp_test_each(fs, hard_link, hard_t, hard_links) { sp_mem_t mem = sp_test_arena(t); - sp_str_t sandbox = sp_test_dir(t); + sp_path_t sandbox = sp_test_dir(t); fs_apply_setup(t, sandbox, it->setup); - sp_str_t target = sp_fs_join_path(mem, sandbox, sp_str_view(it->target)); - sp_str_t link = sp_fs_join_path(mem, sandbox, sp_str_view(it->link)); - - sp_err_t result = it->symlink - ? sp_fs_create_sym_link(target, link) - : sp_fs_create_hard_link(target, link); - sp_expect_err_eq(t, result, it->expect.err); + sp_path_t target = sp_path_join(mem, sandbox, sp_cstr_as_str(it->target)); + sp_path_t link = sp_path_join(mem, sandbox, sp_cstr_as_str(it->link)); + sp_expect_err_eq(t, sp_fs_create_hard_link_at(target, link), it->err); // a hard link shares content with its target: rewrite the target through // one name, then the expected paths observe the update through the other if (it->rewrite) { sp_io_file_writer_t writer = sp_zero; - sp_io_file_writer_from_path(&writer, target); - sp_io_write_str(&writer.base, sp_str_view(it->rewrite), SP_NULLPTR); + sp_io_file_writer_from_path_at(&writer, target); + sp_io_write_str(&writer.base, sp_cstr_as_str(it->rewrite), SP_NULLPTR); sp_io_file_writer_close(&writer); } - fs_expect_paths(t, sandbox, it->expect.paths); + fs_expect_paths(t, sandbox, it->expect); + return SP_OK; +} + +sp_test_each(fs, sym_link, sym_t, sym_links) { + sp_test_skip_without_symlinks(); + + sp_path_t sandbox = sp_test_dir(t); + fs_apply_setup(t, sandbox, it->setup); + + sp_path_t link = sp_path_join(sp_test_arena(t), sandbox, sp_cstr_as_str(it->link)); + sp_expect_err_eq(t, sp_fs_create_sym_link_at(sp_cstr_as_str(it->target), link, it->kind), it->err); + + fs_expect_paths(t, sandbox, it->expect); return SP_OK; } diff --git a/test/fs/mod_time.c b/test/fs/mod_time.c index d7f5200a..861d8d0d 100644 --- a/test/fs/mod_time.c +++ b/test/fs/mod_time.c @@ -1,29 +1,31 @@ #include "fs.h" sp_test(fs, mod_time_nonzero) { - sp_str_t file = fs_path(t, sp_str_lit("A")); - sp_str_t dir = fs_path(t, sp_str_lit("B")); - sp_fs_create_file(file); - sp_fs_create_dir(dir); + sp_mem_t mem = sp_test_arena(t); + sp_path_t sandbox = sp_test_dir(t); + sp_path_t file = sp_path_join(mem, sandbox, sp_str_lit("A")); + sp_path_t dir = sp_path_join(mem, sandbox, sp_str_lit("B")); + sp_fs_create_file_at(file); + sp_fs_create_dir_at(dir); - sp_expect(t, sp_fs_get_mod_time(file).s > 0); - sp_expect(t, sp_fs_get_mod_time(dir).s > 0); + sp_expect(t, sp_fs_get_mod_time_at(file).s > 0); + sp_expect(t, sp_fs_get_mod_time_at(dir).s > 0); return SP_OK; } sp_test(fs, mod_time_updates_after_write) { - sp_str_t file = fs_path(t, sp_str_lit("A")); - sp_fs_create_file_str(file, sp_str_lit("A")); + sp_path_t file = sp_path_join(sp_test_arena(t), sp_test_dir(t), sp_str_lit("A")); + sp_fs_create_file_str_at(file, sp_str_lit("A")); - sp_tm_epoch_t before = sp_fs_get_mod_time(file); + sp_tm_epoch_t before = sp_fs_get_mod_time_at(file); sp_os_sleep_ms(100); sp_io_file_writer_t writer = sp_zero; - sp_io_file_writer_from_path(&writer, file); + sp_io_file_writer_from_path_at(&writer, file); sp_io_write_str(&writer.base, sp_str_lit("B"), SP_NULLPTR); sp_io_file_writer_close(&writer); - sp_tm_epoch_t after = sp_fs_get_mod_time(file); + sp_tm_epoch_t after = sp_fs_get_mod_time_at(file); sp_must(t, after.s > before.s || (after.s == before.s && after.ns > before.ns)); return SP_OK; } diff --git a/test/fs/normalize_path.c b/test/fs/normalize_path.c index b470c78b..e3a4958f 100644 --- a/test/fs/normalize_path.c +++ b/test/fs/normalize_path.c @@ -3,32 +3,32 @@ typedef struct { const c8* name; + sp_fs_path_kind_t kind; const c8* input; const c8* expect; } test_t; static const test_t tests [] = { - { .name = "empty", .input = "", .expect = "" }, - { .name = "bare", .input = "A", .expect = "A" }, - { .name = "trailing_slash", .input = "A/", .expect = "A" }, - { .name = "nested", .input = "A/B", .expect = "A/B" }, - { .name = "nested_ext", .input = "A/B.txt", .expect = "A/B.txt" }, - { .name = "hidden", .input = ".A", .expect = ".A" }, - { .name = "drive_forward", .input = "C:/A/B.txt", .expect = "C:/A/B.txt" }, - { .name = "drive_backslash", .input = "C:\\A\\B.txt", .expect = "C:/A/B.txt" }, - { .name = "mixed_separators", .input = "C:/A\\B/C\\D.txt", .expect = "C:/A/B/C/D.txt" }, - { .name = "drive_backslash_trailing", .input = "C:\\A\\B\\", .expect = "C:/A/B" }, - { .name = "backslash", .input = "A\\B", .expect = "A/B" }, - { .name = "root_backslash", .input = "\\", .expect = "" }, - { .name = "root_slash", .input = "/", .expect = "" }, - { .name = "backslash_trailing", .input = "A\\", .expect = "A" }, - { .name = "preserves_dotdot", .input = "A\\B\\..\\C", .expect = "A/B/../C" }, - { .name = "preserves_dot", .input = "A\\.\\B", .expect = "A/./B" }, - { .name = "nonexistent_path", .input = "C:\\no\\such\\path\\file.txt", .expect = "C:/no/such/path/file.txt" }, + { .name = "empty", .input = "", .expect = "" }, + { .name = "bare", .input = "A", .expect = "A" }, + { .name = "nested", .input = "A/B", .expect = "A/B" }, + { .name = "trailing_slash", .input = "A/", .expect = "A" }, + { .name = "root", .input = "/", .expect = "/" }, + { .name = "keeps_separator_run", .input = "A//B", .expect = "A//B" }, + { .name = "keeps_dot", .input = "A/./B", .expect = "A/./B" }, + { .name = "keeps_dotdot", .input = "A/B/../C", .expect = "A/B/../C" }, + { .name = "backslash_is_a_name", .input = "A\\B\\", .expect = "A\\B\\" }, + + { .name = "windows_backslash", .kind = SP_FS_PATH_WINDOWS, .input = "A\\B", .expect = "A/B" }, + { .name = "windows_mixed", .kind = SP_FS_PATH_WINDOWS, .input = "C:/A\\B/C\\D.txt", .expect = "C:/A/B/C/D.txt" }, + { .name = "windows_trailing", .kind = SP_FS_PATH_WINDOWS, .input = "C:\\A\\B\\", .expect = "C:/A/B" }, + { .name = "windows_root", .kind = SP_FS_PATH_WINDOWS, .input = "\\", .expect = "/" }, + { .name = "windows_drive", .kind = SP_FS_PATH_WINDOWS, .input = "C:\\", .expect = "C:/" }, + { .name = "windows_keeps_dotdot", .kind = SP_FS_PATH_WINDOWS, .input = "A\\B\\..\\C", .expect = "A/B/../C" }, }; sp_test_each(fs, normalize_path, test_t, tests) { - sp_str_t result = sp_fs_normalize_path(sp_test_arena(t), sp_str_view(it->input)); + sp_str_t result = sp_fs_normalize_path_for(sp_test_arena(t), sp_cstr_as_str(it->input), it->kind); sp_expect_str_eq_c(t, result, it->expect); return SP_OK; } diff --git a/test/fs/parent_path.c b/test/fs/parent_path.c index aa429034..6689a6aa 100644 --- a/test/fs/parent_path.c +++ b/test/fs/parent_path.c @@ -3,28 +3,36 @@ typedef struct { const c8* name; + sp_fs_path_kind_t kind; const c8* input; const c8* expect; } test_t; static const test_t tests [] = { - { .name = "empty", .input = "", .expect = "" }, - { .name = "bare", .input = "A", .expect = "" }, - { .name = "trailing_slash", .input = "A/", .expect = "" }, - { .name = "nested", .input = "A/B", .expect = "A" }, - { .name = "nested_ext", .input = "A/B.txt", .expect = "A" }, - // backslash: no '/' found so the parent is empty - { .name = "backslash_not_separator", .input = "C:\\A\\B.txt", .expect = "" }, - { .name = "drive_nested", .input = "C:/A/B/C.txt", .expect = "C:/A/B" }, - { .name = "drive_trailing_slash", .input = "C:/A/B/", .expect = "C:/A" }, - { .name = "drive_multi_trailing", .input = "C:/A/B///", .expect = "C:/A" }, - { .name = "drive_root", .input = "C:/", .expect = "C:/" }, - { .name = "root", .input = "/", .expect = "/" }, - { .name = "absolute_single", .input = "/A", .expect = "" }, - { .name = "absolute_nested", .input = "/A/B/C", .expect = "/A/B" }, + { .name = "empty", .input = "", .expect = "" }, + { .name = "bare", .input = "A", .expect = "." }, + { .name = "bare_trailing", .input = "A/", .expect = "." }, + { .name = "dot", .input = ".", .expect = "." }, + { .name = "dotdot", .input = "..", .expect = "." }, + { .name = "nested", .input = "A/B", .expect = "A" }, + { .name = "nested_trailing", .input = "A/B/", .expect = "A" }, + { .name = "separator_run", .input = "A//B", .expect = "A" }, + { .name = "root", .input = "/", .expect = "/" }, + { .name = "under_root", .input = "/A", .expect = "/" }, + { .name = "absolute_nested", .input = "/A/B/C", .expect = "/A/B" }, + { .name = "backslash_is_a_name", .input = "A\\B", .expect = "." }, + { .name = "drive_is_a_name", .input = "C:/A", .expect = "C:" }, + + { .name = "windows_backslash", .kind = SP_FS_PATH_WINDOWS, .input = "A\\B", .expect = "A" }, + { .name = "windows_mixed", .kind = SP_FS_PATH_WINDOWS, .input = "A/B\\C", .expect = "A/B" }, + { .name = "windows_root", .kind = SP_FS_PATH_WINDOWS, .input = "\\", .expect = "\\" }, + { .name = "windows_under_root", .kind = SP_FS_PATH_WINDOWS, .input = "\\A", .expect = "\\" }, + { .name = "windows_drive", .kind = SP_FS_PATH_WINDOWS, .input = "C:/", .expect = "C:/" }, + { .name = "windows_under_drive", .kind = SP_FS_PATH_WINDOWS, .input = "C:\\A", .expect = "C:\\" }, + { .name = "windows_drive_nested", .kind = SP_FS_PATH_WINDOWS, .input = "C:/A/B///", .expect = "C:/A" }, }; sp_test_each(fs, parent_path, test_t, tests) { - sp_expect_str_eq_c(t, sp_fs_parent_path(sp_str_view(it->input)), it->expect); + sp_expect_str_eq_c(t, sp_fs_parent_path_for(sp_cstr_as_str(it->input), it->kind), it->expect); return SP_OK; } diff --git a/test/fs/path_from_str.c b/test/fs/path_from_str.c new file mode 100644 index 00000000..01d46eb7 --- /dev/null +++ b/test/fs/path_from_str.c @@ -0,0 +1,184 @@ +#include "sp.h" +#include "sp/sp_test.h" + +#define MAX_ROOTS 4 +#define ROOT_FD 9000 + +typedef enum { + DIR_ROOT, + DIR_NONE, +} dir_t; + +typedef struct { + const c8* label; + sp_err_t err; +} root_t; + +typedef struct { + dir_t dir; + s32 root; + const c8* sub; +} expect_t; + +typedef struct { + const c8* name; + root_t roots [MAX_ROOTS]; + const c8* path; + expect_t expect; +} test_t; + +static const test_t* active; + +static s32 num_roots() { + s32 n = 0; + sp_carr_for(active->roots, it) { + if (!active->roots[it].label && !active->roots[it].err) break; + n++; + } + return n; +} + +static sp_sys_fd_t get_root(s32 it) { + if (it < 0 || it >= num_roots()) return SP_SYS_INVALID_FD; + return (sp_sys_fd_t)(ROOT_FD + it); +} + +static sp_err_t get_root_label(s32 it, c8* buf, u64 size, u64* len) { + *len = 0; + if (it < 0 || it >= num_roots()) return SP_ERR_SYS_BAD_FD; + + const root_t* root = &active->roots[it]; + if (root->err) return root->err; + + u64 n = sp_cstr_len(root->label); + if (n >= size) return SP_ERR_SYS_NAME_TOO_LONG; + sp_mem_copy(buf, root->label, n); + buf[n] = 0; + *len = n; + return SP_OK; +} + +static const test_t tests [] = { + { + .name = "relative_goes_to_root_zero", + .roots = { { .label = "/A" } }, + .path = "A/B", + .expect = { .sub = "A/B" }, + }, + { + .name = "relative_ignores_relative_labels", + .roots = { { .label = "." }, { .label = "A" } }, + .path = "A/B", + .expect = { .sub = "A/B" }, + }, + { + .name = "relative_without_roots", + .path = "A", + .expect = { .dir = DIR_NONE, .sub = "A" }, + }, + { + .name = "label_is_stripped", + .roots = { { .label = "/B" }, { .label = "/A" } }, + .path = "/A/B", + .expect = { .root = 1, .sub = "B" }, + }, + { + .name = "separator_run_is_stripped", + .roots = { { .label = "/B" }, { .label = "/A" } }, + .path = "/A//B", + .expect = { .root = 1, .sub = "B" }, + }, + { + .name = "exact_label_is_dot", + .roots = { { .label = "/B" }, { .label = "/A" } }, + .path = "/A", + .expect = { .root = 1, .sub = "." }, + }, + { + .name = "trailing_separator_is_dot", + .roots = { { .label = "/B" }, { .label = "/A" } }, + .path = "/A/", + .expect = { .root = 1, .sub = "." }, + }, + { + .name = "label_matches_whole_components", + .roots = { { .label = "/B" }, { .label = "/A" } }, + .path = "/AB/C", + .expect = { .dir = DIR_NONE, .sub = "/AB/C" }, + }, + { + .name = "absolute_without_serving_root", + .roots = { { .label = "/A" } }, + .path = "/B/C", + .expect = { .dir = DIR_NONE, .sub = "/B/C" }, + }, + { + .name = "deepest_label_wins", + .roots = { { .label = "/A" }, { .label = "/A/B" } }, + .path = "/A/B/C", + .expect = { .root = 1, .sub = "C" }, + }, + { + .name = "deepest_label_wins_in_any_order", + .roots = { { .label = "/A/B" }, { .label = "/A" } }, + .path = "/A/B/C", + .expect = { .sub = "C" }, + }, + { + .name = "slash_label_strips_the_root", + .roots = { { .label = "/B" }, { .label = "/" } }, + .path = "/A/B", + .expect = { .root = 1, .sub = "A/B" }, + }, + { + .name = "slash_label_is_dot", + .roots = { { .label = "/B" }, { .label = "/" } }, + .path = "/", + .expect = { .root = 1, .sub = "." }, + }, + { + .name = "empty_label_serves_everything", + .roots = { { .label = "/B" }, { .label = "" } }, + .path = "/A/B", + .expect = { .root = 1, .sub = "/A/B" }, + }, + { + .name = "any_label_beats_the_empty_label", + .roots = { { .label = "" }, { .label = "/" } }, + .path = "/A/B", + .expect = { .root = 1, .sub = "A/B" }, + }, + { + .name = "relative_label_never_matches", + .roots = { { .label = "A" }, { .label = "/A" } }, + .path = "/A/B", + .expect = { .root = 1, .sub = "B" }, + }, + { + .name = "unreadable_label_is_skipped", + .roots = { { .err = SP_ERR_SYS_NAME_TOO_LONG }, { .label = "/A" } }, + .path = "/A/B", + .expect = { .root = 1, .sub = "B" }, + }, +}; + +static sp_err_t run(sp_test_t* t, test_t* c) { + sp_sys_vtable_t vt = sp_sys_vtable_platform; + vt.get_root = get_root; + vt.get_root_label = get_root_label; + + active = c; + const sp_sys_vtable_t* saved = sp_sys_set_vtable(&vt); + sp_path_t path = sp_path_from_str(sp_cstr_as_str(c->path)); + sp_sys_set_vtable(saved); + active = SP_NULLPTR; + + switch (c->expect.dir) { + case DIR_ROOT: sp_expect_eq(t, path.dir, (sp_sys_fd_t)(ROOT_FD + c->expect.root)); break; + case DIR_NONE: sp_expect_eq(t, path.dir, SP_SYS_INVALID_FD); break; + } + sp_expect_str_eq(t, path.sub, sp_cstr_as_str(c->expect.sub)); + return SP_OK; +} + +sp_test_each_fn(fs, path_from_str, test_t, tests, run, .serial = true); diff --git a/test/fs/path_join.c b/test/fs/path_join.c new file mode 100644 index 00000000..3a30f4fe --- /dev/null +++ b/test/fs/path_join.c @@ -0,0 +1,29 @@ +#include "sp.h" +#include "sp/sp_test.h" + +#define DIR_FD 9000 + +typedef struct { + const c8* name; + const c8* a; + const c8* b; + const c8* expect; +} test_t; + +static const test_t tests [] = { + { .name = "basic", .a = "A", .b = "B", .expect = "A/B" }, + { .name = "nested_rhs", .a = "A", .b = "B/C", .expect = "A/B/C" }, + { .name = "dot_lhs", .a = ".", .b = "A", .expect = "./A" }, + { .name = "trailing_lhs", .a = "A/", .b = "B", .expect = "A/B" }, + { .name = "leading_rhs", .a = "A", .b = "/B", .expect = "A/B" }, + { .name = "empty_lhs", .a = "", .b = "A", .expect = "A" }, + { .name = "empty_rhs", .a = "A", .b = "", .expect = "A" }, +}; + +sp_test_each(fs, path_join, test_t, tests) { + sp_path_t path = sp_path((sp_sys_fd_t)DIR_FD, sp_cstr_as_str(it->a)); + sp_path_t result = sp_path_join(sp_test_arena(t), path, sp_cstr_as_str(it->b)); + sp_expect_eq(t, result.dir, (sp_sys_fd_t)DIR_FD); + sp_expect_str_eq_c(t, result.sub, it->expect); + return SP_OK; +} diff --git a/test/fs/path_parent.c b/test/fs/path_parent.c new file mode 100644 index 00000000..34e4bf49 --- /dev/null +++ b/test/fs/path_parent.c @@ -0,0 +1,33 @@ +#include "sp.h" +#include "sp/sp_test.h" + +#define DIR_FD 9000 + +typedef struct { + const c8* name; + const c8* sub; + const c8* expect; +} test_t; + +static const test_t tests [] = { + { .name = "nested", .sub = "A/B", .expect = "A" }, + { .name = "bare", .sub = "A", .expect = "." }, + { .name = "trailing_sep", .sub = "A/B/", .expect = "A" }, + { .name = "root_label", .sub = ".", .expect = "./.." }, + { .name = "trailing_dot", .sub = "./", .expect = "./.." }, + { .name = "empty", .sub = "", .expect = "" }, + { .name = "dotdot", .sub = "..", .expect = "../.." }, + { .name = "nested_dotdot", .sub = "A/..", .expect = "A/../.." }, + { .name = "trailing_dotdot", .sub = "A/../", .expect = "A/../.." }, + { .name = "nested_dot", .sub = "A/.", .expect = "A/./.." }, + { .name = "root", .sub = "/", .expect = "/" }, + { .name = "under_root", .sub = "/A", .expect = "/" }, +}; + +sp_test_each(fs, path_parent, test_t, tests) { + sp_path_t path = sp_path((sp_sys_fd_t)DIR_FD, sp_cstr_as_str(it->sub)); + sp_path_t result = sp_path_parent(sp_test_arena(t), path); + sp_expect_eq(t, result.dir, (sp_sys_fd_t)DIR_FD); + sp_expect_str_eq_c(t, result.sub, it->expect); + return SP_OK; +} diff --git a/test/fs/predicates.c b/test/fs/predicates.c index f95b8581..88c3949c 100644 --- a/test/fs/predicates.c +++ b/test/fs/predicates.c @@ -24,7 +24,7 @@ static const test_t tests [] = { { "A" }, { "B", FS_SETUP_DIR }, { .path = "C", .kind = FS_SETUP_SYMLINK, .target = "A" }, - { .path = "D", .kind = FS_SETUP_SYMLINK, .target = "B" }, + { .path = "D", .kind = FS_SETUP_DIR_SYMLINK, .target = "B" }, }, .probes = { { @@ -89,24 +89,26 @@ static const test_t tests [] = { }; sp_test_each(fs, predicates, test_t, tests) { - skip_if_symlinks_needed(t, it->setup); + if (fs_setup_needs_symlinks(it->setup)) sp_test_skip_without_symlinks(); sp_mem_t mem = sp_test_arena(t); - sp_str_t sandbox = sp_test_dir(t); + sp_path_t sandbox = sp_test_dir(t); fs_apply_setup(t, sandbox, it->setup); sp_carr_for(it->probes, i) { const probe_t* probe = &it->probes[i]; if (!probe->path) break; - sp_str_t path = sp_fs_join_path(mem, sandbox, sp_str_view(probe->path)); + sp_str_t label = sp_cstr_as_str(probe->path); + sp_path_t path = sp_path_join(mem, sandbox, label); - fs_expect_bool(t, path, "exists", sp_fs_exists(path), probe->exists); - fs_expect_bool(t, path, "is_regular_file", sp_fs_is_file(path), probe->file); - fs_expect_bool(t, path, "is_dir", sp_fs_is_dir(path), probe->dir); - fs_expect_bool(t, path, "is_symlink", sp_fs_is_symlink(path), probe->symlink); - fs_expect_bool(t, path, "is_target_regular_file", sp_fs_is_target_file(path), probe->target_file); - fs_expect_bool(t, path, "is_target_dir", sp_fs_is_target_dir(path), probe->target_dir); - fs_expect_kind(t, path, sp_fs_get_kind(path), probe->kind); + fs_expect_bool(t, label, "exists", sp_fs_exists_at(path), probe->exists); + fs_expect_bool(t, label, "is_regular_file", sp_fs_is_file_at(path), probe->file); + fs_expect_bool(t, label, "is_dir", sp_fs_is_dir_at(path), probe->dir); + fs_expect_bool(t, label, "is_symlink", sp_fs_is_symlink_at(path), probe->symlink); + fs_expect_bool(t, label, "is_target_regular_file", sp_fs_is_target_file_at(path), probe->target_file); + fs_expect_bool(t, label, "is_target_dir", sp_fs_is_target_dir_at(path), probe->target_dir); + fs_expect_kind(t, label, sp_fs_get_kind_at(path), probe->kind); } + return SP_OK; } diff --git a/test/fs/remove.c b/test/fs/remove.c deleted file mode 100644 index 9dc9e115..00000000 --- a/test/fs/remove.c +++ /dev/null @@ -1,162 +0,0 @@ -#include "fs.h" - -typedef struct { - const c8* name; - fs_setup_t setup [FS_MAX_SETUP]; - bool dir; - const c8* path; - sp_err_t err; - fs_expected_path_t expect [FS_MAX_PATHS]; -} test_t; - -static const test_t tests [] = { - { - .name = "file_basic", - .setup = { - { "A" }, - }, - .path = "A", - .expect = { - { .path = "A" }, - }, - }, - { - .name = "file_missing", - .path = "A", - .err = SP_ERR_SYS_NOT_FOUND, - }, - { - .name = "file_is_dir", - .setup = { - { "A", FS_SETUP_DIR }, - }, - .path = "A", - .err = SP_ERR_SYS_IS_DIR, - .expect = { - { .path = "A", .exists = true, .kind = SP_FS_KIND_DIR }, - }, - }, - { - .name = "dir_recursive", - .setup = { - { "A", FS_SETUP_DIR }, - { "A/B" }, - { "A/C", FS_SETUP_DIR }, - { "A/C/D" }, - }, - .dir = true, - .path = "A", - .expect = { - { .path = "A" }, - { .path = "A/B" }, - { .path = "A/C" }, - { .path = "A/C/D" }, - }, - }, - { - .name = "dir_missing", - .dir = true, - .path = "A", - .err = SP_ERR_SYS_NOT_FOUND, - }, - { - .name = "dir_is_file", - .setup = { - { "A" }, - }, - .dir = true, - .path = "A", - .err = SP_ERR_SYS_NOT_DIR, - .expect = { - { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE }, - }, - }, - { - .name = "dir_does_not_follow_symlink", - .setup = { - { "B" }, - { "A", FS_SETUP_DIR }, - { .path = "A/L", .kind = FS_SETUP_SYMLINK, .target = "B" }, - { "A/C" }, - }, - .dir = true, - .path = "A", - .expect = { - { .path = "A" }, - { .path = "A/L" }, - { .path = "A/C" }, - { .path = "B", .exists = true, .kind = SP_FS_KIND_FILE }, - }, - }, - { - .name = "dir_symlink_root_removes_link_only", - .setup = { - { "A", FS_SETUP_DIR }, - { "A/B" }, - { .path = "L", .kind = FS_SETUP_SYMLINK, .target = "A" }, - }, - .dir = true, - .path = "L", - .expect = { - { .path = "L" }, - { .path = "A", .exists = true, .kind = SP_FS_KIND_DIR }, - { .path = "A/B", .exists = true, .kind = SP_FS_KIND_FILE }, - }, - }, -#if defined(SP_POSIX) - { - .name = "dir_with_fifo", - .setup = { - { "A", FS_SETUP_DIR }, - { .path = "A/F", .kind = FS_SETUP_FIFO }, - { "A/B" }, - }, - .dir = true, - .path = "A", - .expect = { - { .path = "A" }, - { .path = "A/F" }, - { .path = "A/B" }, - }, - }, -#endif - { - .name = "unicode_file", - .setup = { - { "\xc3\xb6\x70\x65\x6e.txt" }, - }, - .path = "\xc3\xb6\x70\x65\x6e.txt", - .expect = { - { .path = "\xc3\xb6\x70\x65\x6e.txt" }, - }, - }, - { - .name = "unicode_dir", - .setup = { - { "\xc3\xa4\x62\x63", FS_SETUP_DIR }, - { "\xc3\xa4\x62\x63/\xc3\xbc\x66\x69\x6c\x65.txt" }, - }, - .dir = true, - .path = "\xc3\xa4\x62\x63", - .expect = { - { .path = "\xc3\xa4\x62\x63" }, - { .path = "\xc3\xa4\x62\x63/\xc3\xbc\x66\x69\x6c\x65.txt" }, - }, - }, -}; - -sp_test_each(fs, remove, test_t, tests) { - skip_if_symlinks_needed(t, it->setup); - - sp_str_t sandbox = sp_test_dir(t); - fs_apply_setup(t, sandbox, it->setup); - - sp_str_t path = sp_fs_join_path(sp_test_arena(t), sandbox, sp_str_view(it->path)); - sp_err_t result = it->dir - ? sp_fs_remove_dir(path) - : sp_fs_remove_file(path); - sp_expect_err_eq(t, result, it->err); - - fs_expect_paths(t, sandbox, it->expect); - return SP_OK; -} diff --git a/test/fs/remove/os.c b/test/fs/remove/os.c new file mode 100644 index 00000000..48339fb3 --- /dev/null +++ b/test/fs/remove/os.c @@ -0,0 +1,400 @@ +#include "fs.h" + +#define DEEP_LEVELS 40 +#define DEEP_NAME "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" \ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + +typedef struct { + sp_err_t err; + fs_expected_path_t paths [FS_MAX_PATHS]; +} expect_t; + +typedef struct { + u32 count; + const c8* dir; +} bulk_t; + +typedef struct { + const c8* name; + fs_setup_t setup [FS_MAX_SETUP]; + bool dir; + const c8* cwd; + const c8* path; + bulk_t bulk; + expect_t expect; +} test_t; + +static const test_t tests [] = { + { + .name = "file_basic", + .setup = { + { "A" }, + }, + .path = "A", + .expect = { + .paths = { + { .path = "A" }, + }, + }, + }, + { + .name = "file_missing", + .path = "A", + .expect = { + .err = SP_ERR_SYS_NOT_FOUND, + }, + }, + { + .name = "file_is_dir", + .setup = { + { "A", FS_SETUP_DIR }, + }, + .path = "A", + .expect = { + .err = SP_ERR_SYS_IS_DIR, + .paths = { + { .path = "A", .exists = true, .kind = SP_FS_KIND_DIR }, + }, + }, + }, + { + .name = "dir_recursive", + .setup = { + { "A", FS_SETUP_DIR }, + { "A/B" }, + { "A/C", FS_SETUP_DIR }, + { "A/C/D" }, + { "A/E", FS_SETUP_DIR }, + }, + .dir = true, + .path = "A", + .expect = { + .paths = { + { .path = "A" }, + { .path = "A/B" }, + { .path = "A/C" }, + { .path = "A/C/D" }, + { .path = "A/E" }, + }, + }, + }, + { + .name = "dir_refills_across_batches", + .setup = { + { "A", FS_SETUP_DIR }, + }, + .dir = true, + .path = "A", + .bulk = { .count = 96 }, + .expect = { + .paths = { + { .path = "A" }, + }, + }, + }, + { + .name = "subdir_refills_across_batches", + .setup = { + { "A", FS_SETUP_DIR }, + { "A/B", FS_SETUP_DIR }, + }, + .dir = true, + .path = "A", + .bulk = { .count = 96, .dir = "B" }, + .expect = { + .paths = { + { .path = "A" }, + { .path = "A/B" }, + }, + }, + }, + { + .name = "dir_multi_component_sub", + .setup = { + { "A/B/C" }, + }, + .dir = true, + .path = "A/B", + .expect = { + .paths = { + { .path = "A", .exists = true, .kind = SP_FS_KIND_DIR }, + { .path = "A/B" }, + }, + }, + }, + { + .name = "dir_unicode_child_round_trips", + .setup = { + { "\xc3\xa4\x62\x63", FS_SETUP_DIR }, + { "\xc3\xa4\x62\x63/\xc3\xbc\x66\x69\x6c\x65.txt" }, + }, + .dir = true, + .path = "\xc3\xa4\x62\x63", + .expect = { + .paths = { + { .path = "\xc3\xa4\x62\x63" }, + { .path = "\xc3\xa4\x62\x63/\xc3\xbc\x66\x69\x6c\x65.txt" }, + }, + }, + }, + { + .name = "dir_missing", + .dir = true, + .path = "A", + .expect = { + .err = SP_ERR_SYS_NOT_FOUND, + }, + }, + { + .name = "dir_is_file", + .setup = { + { "A" }, + }, + .dir = true, + .path = "A", + .expect = { + .err = SP_ERR_SYS_NOT_DIR, + .paths = { + { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE }, + }, + }, + }, + { + .name = "dir_dot_rejected", + .setup = { + { "A/B" }, + }, + .dir = true, + .cwd = "A", + .path = ".", + .expect = { + .err = SP_ERR_SYS_INVALID, + .paths = { + { .path = "A/B", .exists = true, .kind = SP_FS_KIND_FILE }, + }, + }, + }, + { + .name = "dir_dotdot_rejected", + .setup = { + { "A/B" }, + }, + .dir = true, + .path = "A/..", + .expect = { + .err = SP_ERR_SYS_INVALID, + .paths = { + { .path = "A/B", .exists = true, .kind = SP_FS_KIND_FILE }, + }, + }, + }, +#if defined(SP_WIN32) + { + .name = "dir_dotdot_backslash_rejected", + .setup = { + { "A/B" }, + }, + .dir = true, + .path = "A\\..", + .expect = { + .err = SP_ERR_SYS_INVALID, + .paths = { + { .path = "A/B", .exists = true, .kind = SP_FS_KIND_FILE }, + }, + }, + }, +#endif + { + .name = "dir_empty_rejected", + .setup = { + { "A/B" }, + }, + .dir = true, + .cwd = "A", + .path = "", + .expect = { + .err = SP_ERR_SYS_INVALID, + .paths = { + { .path = "A/B", .exists = true, .kind = SP_FS_KIND_FILE }, + }, + }, + }, + { + .name = "dir_does_not_follow_symlink", + .setup = { + { "B", FS_SETUP_DIR }, + { "B/C" }, + { "A", FS_SETUP_DIR }, + { .path = "A/L", .kind = FS_SETUP_DIR_SYMLINK, .target = "../B" }, + { "A/D" }, + }, + .dir = true, + .path = "A", + .expect = { + .paths = { + { .path = "A" }, + { .path = "A/L" }, + { .path = "A/D" }, + { .path = "B", .exists = true, .kind = SP_FS_KIND_DIR }, + { .path = "B/C", .exists = true, .kind = SP_FS_KIND_FILE }, + }, + }, + }, + { + .name = "dir_symlink_root_removes_link_only", + .setup = { + { "A", FS_SETUP_DIR }, + { "A/B" }, + { .path = "L", .kind = FS_SETUP_DIR_SYMLINK, .target = "A" }, + }, + .dir = true, + .path = "L", + .expect = { + .paths = { + { .path = "L" }, + { .path = "A", .exists = true, .kind = SP_FS_KIND_DIR }, + { .path = "A/B", .exists = true, .kind = SP_FS_KIND_FILE }, + }, + }, + }, + { + .name = "dir_symlink_root_trailing_sep_removes_link_only", + .setup = { + { "A", FS_SETUP_DIR }, + { "A/B" }, + { .path = "L", .kind = FS_SETUP_DIR_SYMLINK, .target = "A" }, + }, + .dir = true, + .path = "L/", + .expect = { + .paths = { + { .path = "L" }, + { .path = "A", .exists = true, .kind = SP_FS_KIND_DIR }, + { .path = "A/B", .exists = true, .kind = SP_FS_KIND_FILE }, + }, + }, + }, +#if defined(SP_POSIX) + { + .name = "dir_with_fifo", + .setup = { + { "A", FS_SETUP_DIR }, + { .path = "A/F", .kind = FS_SETUP_FIFO }, + { "A/B" }, + }, + .dir = true, + .path = "A", + .expect = { + .paths = { + { .path = "A" }, + { .path = "A/F" }, + { .path = "A/B" }, + }, + }, + }, +#endif +}; + +sp_test_each(fs, remove, test_t, tests) { + if (fs_setup_needs_symlinks(it->setup)) sp_test_skip_without_symlinks(); + + sp_mem_t mem = sp_test_arena(t); + sp_path_t sandbox = sp_test_dir(t); + fs_apply_setup(t, sandbox, it->setup); + + sp_path_t base = it->cwd ? sp_path_join(mem, sandbox, sp_cstr_as_str(it->cwd)) : sandbox; + sp_path_t target = sp_path_join(mem, base, sp_cstr_as_str(it->path)); + sp_path_t into = it->bulk.dir ? sp_path_join(mem, target, sp_cstr_as_str(it->bulk.dir)) : target; + sp_for(b, it->bulk.count) { + sp_str_t name = sp_fmt(mem, "R{}", sp_fmt_uint(b)).value; + sp_must_ok(t, sp_fs_create_file_at(sp_path_join(mem, into, name))); + } + sp_sys_fd_t dir = SP_SYS_INVALID_FD; + sp_must_ok(t, sp_fs_open_dir_at(base, &dir)); + + sp_path_t path = sp_path(dir, sp_cstr_as_str(it->path)); + sp_err_t result = it->dir ? sp_fs_remove_dir_at(path) : sp_fs_remove_file_at(path); + sp_expect_err_eq(t, result, it->expect.err); + + sp_sys_file_meta_t meta = sp_zero; + sp_expect_ok(t, sp_sys_get_file_metadata(dir, &meta)); + sp_sys_close(dir); + + fs_expect_paths(t, sandbox, it->expect.paths); + return SP_OK; +} + +sp_test(fs, remove_dir_deeper_than_path_max_is_name_too_long) { + sp_path_t root = sp_path_join(sp_test_arena(t), sp_test_dir(t), sp_str_lit("A")); + sp_must_ok(t, sp_fs_create_dir_at(root)); + + sp_sys_fd_t cur = SP_SYS_INVALID_FD; + sp_must_ok(t, sp_fs_open_dir_at(root, &cur)); + sp_for(level, DEEP_LEVELS) { + sp_path_t child = sp_path(cur, sp_str_lit(DEEP_NAME)); + sp_must_ok(t, sp_fs_create_dir_at(child)); + + sp_sys_fd_t next = SP_SYS_INVALID_FD; + sp_must_ok(t, sp_fs_open_dir_at(child, &next)); + sp_sys_close(cur); + cur = next; + } + sp_must_ok(t, sp_fs_create_file_at(sp_path(cur, sp_str_lit("F")))); + sp_sys_close(cur); + + sp_expect_err_eq(t, sp_fs_remove_dir_at(root), SP_ERR_SYS_NAME_TOO_LONG); + sp_expect(t, sp_fs_exists_at(root)); + + sp_must_ok(t, sp_fs_open_dir_at(root, &cur)); + sp_for(level, DEEP_LEVELS / 2) { + sp_sys_fd_t next = SP_SYS_INVALID_FD; + sp_must_ok(t, sp_sys_open_dir_s(cur, sp_str_lit(DEEP_NAME), 0, &next)); + sp_sys_close(cur); + cur = next; + } + sp_expect_ok(t, sp_fs_remove_dir_at(sp_path(cur, sp_str_lit(DEEP_NAME)))); + sp_sys_close(cur); + + sp_expect_ok(t, sp_fs_remove_dir_at(root)); + sp_expect(t, !sp_fs_exists_at(root)); + return SP_OK; +} + +#if defined(SP_POSIX) +sp_test(fs, remove_dir_unwritable_subdir_fails) { + sp_mem_t mem = sp_test_arena(t); + sp_path_t sandbox = sp_test_dir(t); + sp_path_t locked = sp_path_join(mem, sandbox, sp_str_lit("A/B")); + sp_must_ok(t, sp_fs_create_dir_at(locked)); + sp_must_ok(t, sp_fs_create_file_at(sp_path_join(mem, sandbox, sp_str_lit("A/B/C")))); + sp_must_ok(t, sp_sys_set_file_perms_s(locked.dir, locked.sub, (sp_sys_file_perms_t) { .value = 0555 })); + + sp_err_t result = sp_fs_remove_dir_at(sp_path_join(mem, sandbox, sp_str_lit("A"))); + + if (!result) return sp_test_skip(t, "directory permissions not enforced"); + sp_must_ok(t, sp_sys_set_file_perms_s(locked.dir, locked.sub, (sp_sys_file_perms_t) { .value = 0755 })); + sp_expect_err_eq(t, result, SP_ERR_SYS_ACCESS_DENIED); + fs_expect_paths(t, sandbox, (const fs_expected_path_t [FS_MAX_PATHS]) { + { .path = "A/B", .exists = true, .kind = SP_FS_KIND_DIR }, + { .path = "A/B/C", .exists = true, .kind = SP_FS_KIND_FILE }, + }); + return SP_OK; +} + +sp_test(fs, remove_dir_unreadable_empty_subdir_is_removed) { + sp_mem_t mem = sp_test_arena(t); + sp_path_t sandbox = sp_test_dir(t); + sp_path_t locked = sp_path_join(mem, sandbox, sp_str_lit("A/B")); + sp_must_ok(t, sp_fs_create_dir_at(locked)); + sp_must_ok(t, sp_sys_set_file_perms_s(locked.dir, locked.sub, (sp_sys_file_perms_t) { .value = 0 })); + + sp_err_t result = sp_fs_remove_dir_at(sp_path_join(mem, sandbox, sp_str_lit("A"))); + + sp_sys_set_file_perms_s(locked.dir, locked.sub, (sp_sys_file_perms_t) { .value = 0755 }); + sp_expect_ok(t, result); + fs_expect_paths(t, sandbox, (const fs_expected_path_t [FS_MAX_PATHS]) { + { .path = "A" }, + }); + return SP_OK; +} +#endif diff --git a/test/fs/remove/sim.c b/test/fs/remove/sim.c new file mode 100644 index 00000000..8b2d9e2d --- /dev/null +++ b/test/fs/remove/sim.c @@ -0,0 +1,319 @@ +#include "sp.h" +#include "sp/sp_test.h" +#include "sim.h" + +typedef struct { + const c8* path; + sim_op_t op; +} removed_t; + +typedef struct { + sp_err_t err; + removed_t removed [SIM_MAX_REMOVED]; + u32 opens; + u32 rmdirs; +} expect_t; + +typedef struct { + const c8* name; + sim_dir_t dirs [SIM_MAX_DIRS]; + expect_t expect; +} test_t; + +static const test_t tests [] = { + { + .name = "entry_vanished_is_ok", + .dirs = { + { .path = "T", .entries = { { "B", SP_FS_KIND_FILE, .absent = true }, { "C", SP_FS_KIND_FILE } } }, + }, + .expect = { + .removed = { { "T/C", SIM_OP_UNLINK }, { "T", SIM_OP_RMDIR } }, + .opens = 1, + .rmdirs = 1, + }, + }, + { + .name = "dir_vanished_before_open_is_ok", + .dirs = { + { .path = "T", .entries = { { "B", SP_FS_KIND_DIR, .absent = true } } }, + }, + .expect = { + .removed = { { "T", SIM_OP_RMDIR } }, + .opens = 2, + .rmdirs = 1, + }, + }, + { + .name = "root_vanished_after_walk_is_ok", + .dirs = { + { .path = "T", .rmdir = SP_ERR_SYS_NOT_FOUND, .entries = { { "B", SP_FS_KIND_FILE } } }, + }, + .expect = { + .removed = { { "T/B", SIM_OP_UNLINK } }, + .opens = 1, + .rmdirs = 1, + }, + }, + { + .name = "root_vanished_before_open_is_not_found", + .dirs = { + { .path = "T", .open = SP_ERR_SYS_NOT_FOUND }, + }, + .expect = { + .err = SP_ERR_SYS_NOT_FOUND, + .opens = 1, + }, + }, + { + .name = "root_vanished_before_retry_is_ok", + .dirs = { + { .path = "T", .rmdir = SP_ERR_SYS_NOT_EMPTY, .reopen = SP_ERR_SYS_NOT_FOUND, .entries = { { "A", SP_FS_KIND_FILE } } }, + }, + .expect = { + .removed = { { "T/A", SIM_OP_UNLINK } }, + .opens = 2, + .rmdirs = 1, + }, + }, + { + .name = "file_hint_that_is_dir_is_walked", + .dirs = { + { .path = "T", .entries = { { "B", SP_FS_KIND_FILE, .unlink = SP_ERR_SYS_IS_DIR } } }, + { .path = "T/B", .entries = { { "C", SP_FS_KIND_FILE } } }, + }, + .expect = { + .removed = { { "T/B/C", SIM_OP_UNLINK }, { "T/B", SIM_OP_RMDIR }, { "T", SIM_OP_RMDIR } }, + .opens = 2, + .rmdirs = 2, + }, + }, + { + .name = "dir_hint_that_is_not_dir_is_unlinked", + .dirs = { + { .path = "T", .entries = { { "B", SP_FS_KIND_DIR } } }, + { .path = "T/B", .open = SP_ERR_SYS_NOT_DIR }, + }, + .expect = { + .removed = { { "T/B", SIM_OP_UNLINK }, { "T", SIM_OP_RMDIR } }, + .opens = 2, + .rmdirs = 1, + }, + }, + { + .name = "dir_hint_that_is_symlink_is_unlinked", + .dirs = { + { .path = "T", .entries = { { "B", SP_FS_KIND_DIR } } }, + { .path = "T/B", .open = SP_ERR_SYS_LOOP }, + }, + .expect = { + .removed = { { "T/B", SIM_OP_UNLINK }, { "T", SIM_OP_RMDIR } }, + .opens = 2, + .rmdirs = 1, + }, + }, + { + .name = "not_empty_without_progress_propagates", + .dirs = { + { .path = "T", .rmdir = SP_ERR_SYS_NOT_EMPTY }, + }, + .expect = { + .err = SP_ERR_SYS_NOT_EMPTY, + .opens = 1, + .rmdirs = 1, + }, + }, + { + .name = "vanished_entry_is_not_progress", + .dirs = { + { .path = "T", .rmdir = SP_ERR_SYS_NOT_EMPTY, .entries = { { "B", SP_FS_KIND_FILE, .absent = true } } }, + }, + .expect = { + .err = SP_ERR_SYS_NOT_EMPTY, + .opens = 1, + .rmdirs = 1, + }, + }, + { + .name = "vanished_subdir_at_rmdir_is_not_progress", + .dirs = { + { .path = "T", .rmdir = SP_ERR_SYS_NOT_EMPTY, .entries = { { "B", SP_FS_KIND_DIR } } }, + { .path = "T/B", .rmdir = SP_ERR_SYS_NOT_FOUND, .entries = { { "C", SP_FS_KIND_FILE } } }, + }, + .expect = { + .err = SP_ERR_SYS_NOT_EMPTY, + .removed = { { "T/B/C", SIM_OP_UNLINK } }, + .opens = 2, + .rmdirs = 2, + }, + }, + { + .name = "parent_progress_survives_child_pass", + .dirs = { + { .path = "T", .rmdir = SP_ERR_SYS_NOT_EMPTY, .entries = { { "A", SP_FS_KIND_FILE }, { "B", SP_FS_KIND_DIR } } }, + { .path = "T/B", .rmdir = SP_ERR_SYS_NOT_FOUND }, + }, + .expect = { + .err = SP_ERR_SYS_NOT_EMPTY, + .removed = { { "T/A", SIM_OP_UNLINK } }, + .opens = 3, + .rmdirs = 3, + }, + }, + { + .name = "not_empty_after_progress_retries_once", + .dirs = { + { .path = "T", .rmdir = SP_ERR_SYS_NOT_EMPTY, .entries = { { "A", SP_FS_KIND_FILE } } }, + }, + .expect = { + .err = SP_ERR_SYS_NOT_EMPTY, + .removed = { { "T/A", SIM_OP_UNLINK } }, + .opens = 2, + .rmdirs = 2, + }, + }, + { + .name = "subdir_not_empty_after_progress_retries_once", + .dirs = { + { .path = "T", .entries = { { "B", SP_FS_KIND_DIR } } }, + { .path = "T/B", .rmdir = SP_ERR_SYS_NOT_EMPTY, .entries = { { "C", SP_FS_KIND_FILE } } }, + }, + .expect = { + .err = SP_ERR_SYS_NOT_EMPTY, + .removed = { { "T/B/C", SIM_OP_UNLINK } }, + .opens = 3, + .rmdirs = 2, + }, + }, + { + .name = "shifted_listing_is_reopened", + .dirs = { + { .path = "T", .batch = 1, .entries = { { "A", SP_FS_KIND_FILE }, { "B", SP_FS_KIND_FILE } } }, + }, + .expect = { + .removed = { { "T/A", SIM_OP_UNLINK }, { "T/B", SIM_OP_UNLINK }, { "T", SIM_OP_RMDIR } }, + .opens = 2, + .rmdirs = 2, + }, + }, + { + .name = "shifted_subdir_listing_is_reopened", + .dirs = { + { .path = "T", .entries = { { "B", SP_FS_KIND_DIR } } }, + { .path = "T/B", .batch = 1, .entries = { { "A", SP_FS_KIND_FILE }, { "C", SP_FS_KIND_FILE } } }, + }, + .expect = { + .removed = { { "T/B/A", SIM_OP_UNLINK }, { "T/B/C", SIM_OP_UNLINK }, { "T/B", SIM_OP_RMDIR }, { "T", SIM_OP_RMDIR } }, + .opens = 3, + .rmdirs = 3, + }, + }, + { + .name = "removed_subdir_shifts_parent_listing", + .dirs = { + { .path = "T", .batch = 1, .entries = { { "B", SP_FS_KIND_DIR }, { "C", SP_FS_KIND_FILE } } }, + { .path = "T/B" }, + }, + .expect = { + .removed = { { "T/B", SIM_OP_RMDIR }, { "T/C", SIM_OP_UNLINK }, { "T", SIM_OP_RMDIR } }, + .opens = 3, + .rmdirs = 3, + }, + }, + { + .name = "unlink_error_propagates_without_retry", + .dirs = { + { .path = "T", .entries = { { "A", SP_FS_KIND_FILE }, { "B", SP_FS_KIND_FILE, .unlink = SP_ERR_SYS_ACCESS_DENIED } } }, + }, + .expect = { + .err = SP_ERR_SYS_ACCESS_DENIED, + .removed = { { "T/A", SIM_OP_UNLINK } }, + .opens = 1, + }, + }, + { + .name = "rmdir_error_propagates_without_retry", + .dirs = { + { .path = "T", .entries = { { "B", SP_FS_KIND_DIR } } }, + { .path = "T/B", .rmdir = SP_ERR_SYS_ACCESS_DENIED, .entries = { { "C", SP_FS_KIND_FILE } } }, + }, + .expect = { + .err = SP_ERR_SYS_ACCESS_DENIED, + .removed = { { "T/B/C", SIM_OP_UNLINK } }, + .opens = 2, + .rmdirs = 1, + }, + }, + { + .name = "unreadable_empty_subdir_is_removed", + .dirs = { + { .path = "T", .entries = { { "B", SP_FS_KIND_DIR } } }, + { .path = "T/B", .open = SP_ERR_SYS_ACCESS_DENIED }, + }, + .expect = { + .removed = { { "T/B", SIM_OP_RMDIR }, { "T", SIM_OP_RMDIR } }, + .opens = 2, + .rmdirs = 2, + }, + }, + { + .name = "unreadable_empty_root_is_removed", + .dirs = { + { .path = "T", .open = SP_ERR_SYS_ACCESS_DENIED }, + }, + .expect = { + .removed = { { "T", SIM_OP_RMDIR } }, + .opens = 1, + .rmdirs = 1, + }, + }, + { + .name = "unreadable_subdir_with_entries_is_access_denied", + .dirs = { + { .path = "T", .entries = { { "B", SP_FS_KIND_DIR } } }, + { .path = "T/B", .open = SP_ERR_SYS_ACCESS_DENIED, .entries = { { "C", SP_FS_KIND_FILE } } }, + }, + .expect = { + .err = SP_ERR_SYS_ACCESS_DENIED, + .opens = 2, + .rmdirs = 1, + }, + }, + { + .name = "read_error_fails_fast_and_unwinds", + .dirs = { + { .path = "T", .entries = { { "B", SP_FS_KIND_DIR }, { "C", SP_FS_KIND_FILE } } }, + { .path = "T/B", .read = SP_ERR_SYS_IO }, + }, + .expect = { + .err = SP_ERR_SYS_IO, + .opens = 2, + }, + }, +}; + +sp_test_each(fs, remove_sim, test_t, tests, .serial = true) { + sim_t s = sp_zero; + sim_begin(&s, it->dirs); + sp_err_t err = sp_fs_remove_dir_at(sp_path(sp_sys_get_root(0), sp_str_lit("T"))); + sim_end(&s); + + sp_expect_err_eq(t, err, it->expect.err); + + u32 expected = 0; + sp_carr_for(it->expect.removed, e) { + if (!it->expect.removed[e].path) break; + expected++; + } + sp_expect_eq(t, s.num_removed, expected); + sp_for(i, sp_min(s.num_removed, expected)) { + sp_expect_str_eq_c(t, sp_cstr_as_str(s.removed[i].path), it->expect.removed[i].path); + sp_expect_eq(t, (u32)s.removed[i].op, (u32)it->expect.removed[i].op); + } + + sp_expect_eq(t, s.count.opens, it->expect.opens); + sp_expect_eq(t, s.count.nofollow, s.count.opens); + sp_expect_eq(t, s.count.rmdirs, it->expect.rmdirs); + sp_expect_eq(t, s.count.closes, s.count.dirs); + sp_expect_eq(t, s.count.fd_closes, (u32)0); + return SP_OK; +} diff --git a/test/fs/replace_ext.c b/test/fs/replace_ext.c index e9776e53..0737faf7 100644 --- a/test/fs/replace_ext.c +++ b/test/fs/replace_ext.c @@ -3,24 +3,28 @@ typedef struct { const c8* name; + sp_fs_path_kind_t kind; const c8* input; const c8* ext; const c8* expect; } test_t; static const test_t tests [] = { - { .name = "swap", .input = "A.c", .ext = "o", .expect = "A.o" }, - { .name = "strip", .input = "A.c", .ext = "", .expect = "A" }, - { .name = "add", .input = "A", .ext = "txt", .expect = "A.txt" }, - { .name = "trailing_dot", .input = "A.", .ext = "txt", .expect = "A.txt" }, - { .name = "double_dot", .input = "A..txt", .ext = "md", .expect = "A..md" }, - { .name = "multiple_dots", .input = "A.B.C", .ext = "d", .expect = "A.B.d" }, - { .name = "hidden", .input = ".A", .ext = "txt", .expect = ".txt" }, - { .name = "nested", .input = "A/B.txt", .ext = "md", .expect = "A/B.md" }, + { .name = "swap", .input = "A.c", .ext = "o", .expect = "A.o" }, + { .name = "strip", .input = "A.c", .ext = "", .expect = "A" }, + { .name = "add", .input = "A", .ext = "txt", .expect = "A.txt" }, + { .name = "trailing_dot", .input = "A.", .ext = "txt", .expect = "A.txt" }, + { .name = "hidden", .input = ".A", .ext = "txt", .expect = ".A.txt" }, + { .name = "keeps_dir", .input = "A.B/C.c", .ext = "o", .expect = "A.B/C.o" }, + { .name = "trailing_slash", .input = "A/B.txt/", .ext = "md", .expect = "A/B.md" }, + { .name = "backslash_is_a_name", .input = "A.B\\C", .ext = "d", .expect = "A.d" }, + + { .name = "windows_keeps_dir", .kind = SP_FS_PATH_WINDOWS, .input = "A.B\\C", .ext = "d", .expect = "A.B\\C.d" }, + { .name = "windows_trailing", .kind = SP_FS_PATH_WINDOWS, .input = "A\\B.c\\", .ext = "o", .expect = "A\\B.o" }, }; sp_test_each(fs, replace_ext, test_t, tests) { - sp_str_t result = sp_fs_replace_ext(sp_test_arena(t), sp_str_view(it->input), sp_str_view(it->ext)); + sp_str_t result = sp_fs_replace_ext_for(sp_test_arena(t), sp_cstr_as_str(it->input), sp_cstr_as_str(it->ext), it->kind); sp_expect_str_eq_c(t, result, it->expect); return SP_OK; } diff --git a/test/fs/sim.c b/test/fs/sim.c index 87e59508..831a8efd 100644 --- a/test/fs/sim.c +++ b/test/fs/sim.c @@ -26,20 +26,57 @@ static const sim_entry_t* find_entry(sp_str_t path) { return SP_NULLPTR; } -static u32 dir_len(const sim_dir_t* dir) { - u32 n = 0; +static s64 slot_of(sp_sys_fd_t fd) { + return (s64)fd - FD_BASE; +} + +static sp_str_t resolve(sp_mem_t mem, sp_sys_fd_t fd, const c8* path, u32 len) { + if (fd == sp_sys_get_root(0)) return sp_str(path, len); + s64 slot = slot_of(fd); + sp_assert(slot >= 0 && slot < (s64)active->count.dirs); + return sp_fs_join_path(mem, sp_cstr_as_str(active->opened[slot].dir->path), sp_str(path, len)); +} + +static void mark_gone(sp_str_t path) { + const sim_dir_t* dir = find_dir(sp_fs_parent_path(path)); + const sim_entry_t* entry = find_entry(path); + if (entry) active->gone[dir - active->dirs][entry - dir->entries] = true; +} + +static void record(sp_str_t path, sim_op_t op) { + sp_assert(active->num_removed < SIM_MAX_REMOVED); + sp_assert(path.len < SIM_MAX_REMOVED_PATH); + sim_removed_t* out = &active->removed[active->num_removed++]; + sp_str_copy_to(path, out->path, SIM_MAX_REMOVED_PATH); + out->op = op; + mark_gone(path); +} + +static bool dir_empty(const sim_dir_t* dir) { sp_carr_for(dir->entries, it) { - if (!dir->entries[it].name) break; - n++; + const sim_entry_t* entry = &dir->entries[it]; + if (!entry->name) break; + if (entry->absent) continue; + if (!active->gone[dir - active->dirs][it]) return false; } - return n; + return true; } -static sp_err_t open_dir(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_fd_t* out) { +static sp_err_t open_dir(sp_sys_fd_t fd, const c8* path, u32 len, u32 flags, sp_sys_fd_t* out) { active->count.opens++; - const sim_dir_t* dir = find_dir(sp_str(path, len)); + if (flags & SP_SYS_OPEN_DIR_NO_FOLLOW) active->count.nofollow++; + if (active->count.dirs == SIM_MAX_OPENS) return SP_ERR_SYS_TOO_MANY_FILES; + sp_mem_arena_marker_t s = sp_mem_begin_scratch(); + sp_str_t full = resolve(s.mem, fd, path, len); + const sim_entry_t* entry = find_entry(full); + const sim_dir_t* dir = find_dir(full); + sp_mem_end_scratch(s); + if (entry && entry->absent) return SP_ERR_SYS_NOT_FOUND; if (!dir) return SP_ERR_SYS_NOT_FOUND; - sp_assert(active->count.dirs < SIM_MAX_OPENS); + if (dir->open) return dir->open; + sp_for(o, active->count.dirs) { + if (active->opened[o].dir == dir && dir->reopen) return dir->reopen; + } active->opened[active->count.dirs] = (sim_open_t) { .dir = dir }; *out = (sp_sys_fd_t)(FD_BASE + active->count.dirs); active->count.dirs++; @@ -51,55 +88,112 @@ static sp_err_t close_fd(sp_sys_fd_t fd) { return SP_OK; } -static sp_err_t dir_from_fd(sp_sys_fd_t fd, sp_sys_dir_t* out) { - s64 handle = (s64)fd - FD_BASE; - if (active->opened[handle].dir->from_fd) return active->opened[handle].dir->from_fd; - *out = sp_zero_s(sp_sys_dir_t); - out->handle = handle; +static sp_err_t dir_it_open(sp_sys_fd_t fd, sp_sys_dir_it_t* out) { + s64 slot = slot_of(fd); + if (active->opened[slot].dir->it_open) return active->opened[slot].dir->it_open; + *out = sp_zero_s(sp_sys_dir_it_t); + out->fd = fd; + out->state = slot; return SP_OK; } -static sp_err_t dir_read(sp_sys_dir_t* dir, sp_mem_buffer_t* buf) { - sim_open_t* slot = &active->opened[dir->handle]; - if (slot->dir->read) return slot->dir->read; - buf->len = slot->served ? 0 : dir_len(slot->dir); - slot->served = true; +static sp_err_t dir_it_read(sp_sys_dir_it_t* it, sp_mem_buffer_t* buf) { + sim_open_t* slot = &active->opened[it->state]; + const sim_dir_t* dir = slot->dir; + if (dir->read) return dir->read; + + u32 batch = dir->batch ? dir->batch : SIM_MAX_ENTRIES; + u32 position = 0; + buf->len = 0; + sp_carr_for(dir->entries, e) { + if (!dir->entries[e].name) break; + if (active->gone[dir - active->dirs][e]) continue; + if (position++ < slot->position) continue; + if (buf->len == batch) break; + slot->listed[buf->len++] = e; + } + slot->position += (u32)buf->len; return SP_OK; } -static sp_err_t dir_parse(sp_sys_dir_t* dir, sp_mem_buffer_t* buf, u64* cursor, sp_sys_dir_entry_t* out) { - const sim_entry_t* entry = &active->opened[dir->handle].dir->entries[*cursor]; +static sp_err_t dir_it_parse(sp_sys_dir_it_t* it, sp_mem_buffer_t* buf, u64* cursor, sp_sys_dir_entry_t* out) { + sim_open_t* slot = &active->opened[it->state]; + const sim_entry_t* entry = &slot->dir->entries[slot->listed[*cursor]]; *cursor += 1; + u32 len = (u32)sp_cstr_len(entry->name); + sp_assert(len <= buf->capacity); + sp_mem_copy(buf->data, entry->name, len); + *out = sp_zero_s(sp_sys_dir_entry_t); - out->name = entry->name; - out->len = (u32)sp_cstr_len(entry->name); + out->name = sp_ptr_cast(const c8*, buf->data); + out->len = len; out->kind = entry->kind; return SP_OK; } -static sp_err_t dir_close(sp_sys_dir_t* dir) { +static sp_err_t dir_it_close(sp_sys_dir_it_t* it) { active->count.closes++; return SP_OK; } static sp_err_t get_link_metadata(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_meta_t* st) { - const sim_entry_t* entry = find_entry(sp_str(path, len)); - if (!entry) return SP_ERR_SYS_NOT_FOUND; + sp_mem_arena_marker_t s = sp_mem_begin_scratch(); + sp_str_t full = resolve(s.mem, fd, path, len); + const sim_entry_t* entry = find_entry(full); + const sim_dir_t* dir = find_dir(full); + sp_mem_end_scratch(s); + *st = sp_zero_s(sp_sys_file_meta_t); - st->kind = entry->stat; - return SP_OK; + if (entry && entry->absent) return SP_ERR_SYS_NOT_FOUND; + if (entry && entry->stat != SP_FS_KIND_NONE) { + st->kind = entry->stat; + return SP_OK; + } + if (dir) { + st->kind = SP_FS_KIND_DIR; + return SP_OK; + } + if (entry) return SP_OK; + return SP_ERR_SYS_NOT_FOUND; +} + +static sp_err_t unlink_entry(sp_sys_fd_t fd, const c8* path, u32 len) { + sp_mem_arena_marker_t s = sp_mem_begin_scratch(); + sp_str_t full = resolve(s.mem, fd, path, len); + const sim_entry_t* entry = find_entry(full); + sp_err_t err = SP_ERR_SYS_NOT_FOUND; + if (entry && !entry->absent) err = entry->unlink; + if (entry && !err) record(full, SIM_OP_UNLINK); + sp_mem_end_scratch(s); + return err; +} + +static sp_err_t remove_dir(sp_sys_fd_t fd, const c8* path, u32 len) { + active->count.rmdirs++; + sp_mem_arena_marker_t s = sp_mem_begin_scratch(); + sp_str_t full = resolve(s.mem, fd, path, len); + const sim_dir_t* dir = find_dir(full); + sp_err_t err = SP_ERR_SYS_NOT_FOUND; + if (dir) err = dir->rmdir; + if (dir && !err && !dir_empty(dir)) err = SP_ERR_SYS_NOT_EMPTY; + if (dir && !err) record(full, SIM_OP_RMDIR); + if (dir && err == SP_ERR_SYS_NOT_FOUND) mark_gone(full); + sp_mem_end_scratch(s); + return err; } void sim_begin(sim_t* sim, const sim_dir_t* dirs) { *sim = (sim_t) { .dirs = dirs, .vt = sp_sys_vtable_platform }; sim->vt.open_dir = open_dir; sim->vt.close = close_fd; - sim->vt.dir_from_fd = dir_from_fd; - sim->vt.dir_read = dir_read; - sim->vt.dir_parse = dir_parse; - sim->vt.dir_close = dir_close; + sim->vt.dir_it_open = dir_it_open; + sim->vt.dir_it_read = dir_it_read; + sim->vt.dir_it_parse = dir_it_parse; + sim->vt.dir_it_close = dir_it_close; sim->vt.get_link_metadata = get_link_metadata; + sim->vt.unlink = unlink_entry; + sim->vt.rmdir = remove_dir; sim->saved = sp_sys_set_vtable(&sim->vt); active = sim; } diff --git a/test/fs/sim.h b/test/fs/sim.h index 07404c1c..fdb60bce 100644 --- a/test/fs/sim.h +++ b/test/fs/sim.h @@ -5,24 +5,38 @@ #define SIM_MAX_DIRS 4 #define SIM_MAX_ENTRIES 4 -#define SIM_MAX_OPENS 4 +#define SIM_MAX_OPENS 8 +#define SIM_MAX_REMOVED 8 +#define SIM_MAX_REMOVED_PATH 64 + +typedef enum { + SIM_OP_UNLINK, + SIM_OP_RMDIR, +} sim_op_t; typedef struct { const c8* name; sp_fs_kind_t kind; sp_fs_kind_t stat; + sp_err_t unlink; + bool absent; } sim_entry_t; typedef struct { const c8* path; - sp_err_t from_fd; + sp_err_t open; + sp_err_t reopen; + sp_err_t it_open; sp_err_t read; + sp_err_t rmdir; + u32 batch; sim_entry_t entries [SIM_MAX_ENTRIES]; } sim_dir_t; typedef struct { const sim_dir_t* dir; - bool served; + u32 position; + u32 listed [SIM_MAX_ENTRIES]; } sim_open_t; typedef struct { @@ -30,12 +44,22 @@ typedef struct { u32 dirs; u32 closes; u32 fd_closes; + u32 rmdirs; + u32 nofollow; } sim_count_t; +typedef struct { + c8 path [SIM_MAX_REMOVED_PATH]; + sim_op_t op; +} sim_removed_t; + typedef struct { const sim_dir_t* dirs; sim_count_t count; sim_open_t opened [SIM_MAX_OPENS]; + sim_removed_t removed [SIM_MAX_REMOVED]; + u32 num_removed; + bool gone [SIM_MAX_DIRS][SIM_MAX_ENTRIES]; sp_sys_vtable_t vt; const sp_sys_vtable_t* saved; } sim_t; diff --git a/test/fs/stem_ext.c b/test/fs/stem_ext.c new file mode 100644 index 00000000..c9bcf83f --- /dev/null +++ b/test/fs/stem_ext.c @@ -0,0 +1,42 @@ +#include "sp.h" +#include "sp/sp_test.h" + +typedef struct { + const c8* stem; + const c8* ext; +} expect_t; + +typedef struct { + const c8* name; + sp_fs_path_kind_t kind; + const c8* input; + expect_t expect; +} test_t; + +static const test_t tests [] = { + { .name = "empty", .input = "", .expect = { "", "" } }, + { .name = "none", .input = "A", .expect = { "A", "" } }, + { .name = "simple", .input = "A.B", .expect = { "A", "B" } }, + { .name = "nested", .input = "A/B.txt", .expect = { "B", "txt" } }, + { .name = "trailing_slash", .input = "A/B.txt/", .expect = { "B", "txt" } }, + { .name = "last_dot_wins", .input = "A.B.C", .expect = { "A.B", "C" } }, + { .name = "double_dot", .input = "A..txt", .expect = { "A.", "txt" } }, + { .name = "trailing_dot", .input = "A.", .expect = { "A", "" } }, + { .name = "dot_in_dir", .input = "/A/B.C/D", .expect = { "D", "" } }, + { .name = "hidden", .input = ".A", .expect = { ".A", "" } }, + { .name = "hidden_with_ext", .input = ".A.txt", .expect = { ".A", "txt" } }, + { .name = "dot", .input = ".", .expect = { ".", "" } }, + { .name = "dotdot", .input = "..", .expect = { "..", "" } }, + { .name = "root", .input = "/", .expect = { "", "" } }, + { .name = "backslash_is_a_name", .input = "A.B\\C", .expect = { "A", "B\\C" } }, + + { .name = "windows_backslash", .kind = SP_FS_PATH_WINDOWS, .input = "C:\\A\\B.txt", .expect = { "B", "txt" } }, + { .name = "windows_dot_in_dir", .kind = SP_FS_PATH_WINDOWS, .input = "A.B\\C", .expect = { "C", "" } }, +}; + +sp_test_each(fs, stem_ext, test_t, tests) { + sp_str_t path = sp_cstr_as_str(it->input); + sp_expect_str_eq_c(t, sp_fs_get_stem_for(path, it->kind), it->expect.stem); + sp_expect_str_eq_c(t, sp_fs_get_ext_for(path, it->kind), it->expect.ext); + return SP_OK; +} diff --git a/test/fs/system_paths.c b/test/fs/system_paths.c index 1a7a8967..0ec2609e 100644 --- a/test/fs/system_paths.c +++ b/test/fs/system_paths.c @@ -14,21 +14,29 @@ sp_test(fs, system_paths_nonempty) { sp_test_skip_on_wasm() sp_mem_t mem = sp_test_arena(t); - sp_must_gt(t, sp_fs_get_storage_path(mem).len, 0); - sp_must_gt(t, sp_fs_get_config_path(mem).len, 0); + sp_str_t storage = sp_zero; + sp_str_t config = sp_zero; + sp_must_ok(t, sp_fs_get_storage_path(mem, &storage)); + sp_must_ok(t, sp_fs_get_config_path(mem, &config)); + sp_must_gt(t, storage.len, 0); + sp_must_gt(t, config.len, 0); return SP_OK; } sp_test(fs, system_paths_storage_path_normalized) { sp_test_skip_on_wasm() - expect_normalized(t, sp_fs_get_storage_path(sp_test_arena(t)), "storage_path"); + sp_str_t path = sp_zero; + sp_must_ok(t, sp_fs_get_storage_path(sp_test_arena(t), &path)); + expect_normalized(t, path, "storage_path"); return SP_OK; } sp_test(fs, system_paths_config_path_normalized) { sp_test_skip_on_wasm() - expect_normalized(t, sp_fs_get_config_path(sp_test_arena(t)), "config_path"); + sp_str_t path = sp_zero; + sp_must_ok(t, sp_fs_get_config_path(sp_test_arena(t), &path)); + expect_normalized(t, path, "config_path"); return SP_OK; } diff --git a/test/fs/trim_path.c b/test/fs/trim_path.c index f4d7b736..a25ca7ce 100644 --- a/test/fs/trim_path.c +++ b/test/fs/trim_path.c @@ -3,27 +3,34 @@ typedef struct { const c8* name; + sp_fs_path_kind_t kind; const c8* input; const c8* expect; } test_t; static const test_t tests [] = { - { .name = "empty", .input = "", .expect = "" }, - { .name = "bare", .input = "A", .expect = "A" }, - { .name = "trailing_slash", .input = "A/", .expect = "A" }, - { .name = "nested", .input = "A/B", .expect = "A/B" }, - { .name = "hidden", .input = ".A", .expect = ".A" }, - { .name = "drive_forward", .input = "C:/A/B.txt", .expect = "C:/A/B.txt" }, - { .name = "drive_backslash", .input = "C:\\A\\B.txt", .expect = "C:\\A\\B.txt" }, - { .name = "root_slash", .input = "/", .expect = "/" }, - { .name = "drive_slash", .input = "C:/", .expect = "C:/" }, - { .name = "drive_backslash_root", .input = "C:\\", .expect = "C:\\" }, - { .name = "absolute_trailing", .input = "/A/", .expect = "/A" }, - { .name = "multi_slash", .input = "A///", .expect = "A" }, - { .name = "multi_backslash", .input = "A\\\\", .expect = "A" }, + { .name = "empty", .input = "", .expect = "" }, + { .name = "bare", .input = "A", .expect = "A" }, + { .name = "trailing_slash", .input = "A/", .expect = "A" }, + { .name = "trailing_slash_run", .input = "A///", .expect = "A" }, + { .name = "nested", .input = "A/B", .expect = "A/B" }, + { .name = "absolute_trailing", .input = "/A/", .expect = "/A" }, + { .name = "root", .input = "/", .expect = "/" }, + { .name = "root_run", .input = "//", .expect = "/" }, + { .name = "backslash_is_a_name", .input = "A\\", .expect = "A\\" }, + { .name = "drive_is_a_name", .input = "C:/", .expect = "C:" }, + + { .name = "windows_slash", .kind = SP_FS_PATH_WINDOWS, .input = "A/", .expect = "A" }, + { .name = "windows_backslash", .kind = SP_FS_PATH_WINDOWS, .input = "A\\", .expect = "A" }, + { .name = "windows_mixed_run", .kind = SP_FS_PATH_WINDOWS, .input = "A/\\/", .expect = "A" }, + { .name = "windows_root", .kind = SP_FS_PATH_WINDOWS, .input = "\\", .expect = "\\" }, + { .name = "windows_drive", .kind = SP_FS_PATH_WINDOWS, .input = "C:/", .expect = "C:/" }, + { .name = "windows_drive_back", .kind = SP_FS_PATH_WINDOWS, .input = "C:\\", .expect = "C:\\" }, + { .name = "windows_drive_run", .kind = SP_FS_PATH_WINDOWS, .input = "C:\\\\", .expect = "C:\\" }, + { .name = "windows_drive_nested", .kind = SP_FS_PATH_WINDOWS, .input = "C:\\A\\", .expect = "C:\\A" }, }; sp_test_each(fs, trim_path, test_t, tests) { - sp_expect_str_eq_c(t, sp_fs_trim_path(sp_str_view(it->input)), it->expect); + sp_expect_str_eq_c(t, sp_fs_trim_path_for(sp_cstr_as_str(it->input), it->kind), it->expect); return SP_OK; } diff --git a/test/fs/write_atomic.c b/test/fs/write_atomic.c new file mode 100644 index 00000000..56996622 --- /dev/null +++ b/test/fs/write_atomic.c @@ -0,0 +1,72 @@ +#include "fs.h" + +typedef struct { + sp_err_t err; + fs_expected_path_t paths [FS_MAX_PATHS]; +} expect_t; + +typedef struct { + const c8* name; + fs_setup_t setup [FS_MAX_SETUP]; + const c8* path; + const c8* content; + expect_t expect; +} test_t; + +static const test_t tests [] = { + { + .name = "new", + .path = "A", + .content = "A", + .expect = { + .paths = { + { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE, .content = "A" }, + }, + }, + }, + { + .name = "empty", + .path = "A", + .content = "", + .expect = { + .paths = { + { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE, .content = "" }, + }, + }, + }, + { + .name = "missing_parent", + .path = "D/A", + .content = "A", + .expect = { + .err = SP_ERR_SYS_NOT_FOUND, + .paths = { + { .path = "D" }, + }, + }, + }, + { + .name = "replace_existing", + .setup = { + { .path = "A", .content = "old" }, + }, + .path = "A", + .content = "new", + .expect = { + .paths = { + { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE, .content = "new" }, + }, + }, + }, +}; + +sp_test_each(fs, write_atomic, test_t, tests) { + sp_path_t sandbox = sp_test_dir(t); + fs_apply_setup(t, sandbox, it->setup); + + sp_path_t path = sp_path_join(sp_test_arena(t), sandbox, sp_str_view(it->path)); + sp_expect_err_eq(t, sp_fs_write_atomic_at(path, sp_str_view(it->content)), it->expect.err); + fs_expect_paths(t, sandbox, it->expect.paths); + fs_expect_no_temps(t, sandbox); + return SP_OK; +} diff --git a/test/gdb.c b/test/gdb.c index 09e4fbb8..92ee5bd2 100644 --- a/test/gdb.c +++ b/test/gdb.c @@ -41,8 +41,9 @@ typedef struct gdb { } sp_gdb; static sp_str_t gdb_fixture_path(sp_mem_t mem) { - sp_str_t exe = sp_fs_parent_path(sp_fs_get_exe_path(mem)); - sp_str_t fixture = sp_fs_join_path(mem, exe, sp_str_lit("gdb_fixture")); + sp_str_t exe = sp_zero; + sp_fs_get_exe_path(mem, &exe); + sp_str_t fixture = sp_fs_join_path(mem, sp_fs_parent_path(exe), sp_str_lit("gdb_fixture")); return sp_fs_replace_ext(mem, fixture, sp_os_get_executable_ext()); } diff --git a/test/glob.c b/test/glob.c index cba3a996..3f5d6097 100644 --- a/test/glob.c +++ b/test/glob.c @@ -835,6 +835,17 @@ UTEST_F(glob, set_nested_path) { sp_glob_set_free(set); } +UTEST_F(glob, set_extension_matches_hidden_file) { + sp_glob_set_t* set = sp_glob_set_new(ut.mem.tracking); + sp_glob_set_add(set, "*.c"); + sp_glob_set_build(set); + + EXPECT_TRUE(sp_glob_set_match(set, sp_str_lit(".c"))); + EXPECT_TRUE(sp_glob_set_match(set, sp_str_lit("src/.c"))); + + sp_glob_set_free(set); +} + UTEST_F(glob, set_basename_literal) { sp_glob_set_t* set = sp_glob_set_new(ut.mem.tracking); sp_glob_set_add(set, "**/foo"); @@ -973,6 +984,46 @@ UTEST_F(glob, set_duplicate_extension) { sp_glob_set_free(set); } +typedef struct { + const c8* pattern; + const c8* path; +} no_match_test_t; + +void run_no_match_test(int* utest_result, sp_mem_t mem, no_match_test_t t) { + sp_str_t path = sp_cstr_as_str(t.path); + + sp_glob_t* g = sp_glob_new(mem, t.pattern); + EXPECT_FALSE(sp_glob_match(g, path)); + sp_glob_free(g); + + sp_glob_set_t* set = sp_glob_set_new(mem); + sp_glob_set_add(set, t.pattern); + sp_glob_set_build(set); + EXPECT_FALSE(sp_glob_set_match(set, path)); + sp_glob_set_free(set); +} + +UTEST_F(glob, no_match_extension_trailing_slash) { + run_no_match_test(utest_result, ut.mem.tracking, (no_match_test_t) { + .pattern = "*.c", + .path = "A.c/", + }); +} + +UTEST_F(glob, no_match_basename_trailing_slash) { + run_no_match_test(utest_result, ut.mem.tracking, (no_match_test_t) { + .pattern = "**/A", + .path = "B/A/", + }); +} + +UTEST_F(glob, no_match_basename_backslash) { + run_no_match_test(utest_result, ut.mem.tracking, (no_match_test_t) { + .pattern = "**/A", + .path = "B\\A", + }); +} + UTEST_F(glob, parse_empty) { run_parse_test(utest_result, ut.mem.arena, (parse_test_t){ .pattern = "", diff --git a/test/io/discard.c b/test/io/discard.c index 656fc0cd..805aee60 100644 --- a/test/io/discard.c +++ b/test/io/discard.c @@ -83,13 +83,13 @@ UTEST_F(io_discard, eof_with_final_bytes_is_ok) { UTEST_F(io, discard_file_skips_by_position) { { sp_io_file_writer_t w = sp_zero; - ASSERT_EQ(sp_io_file_writer_from_path(&w, ut.file_path), SP_OK); + ASSERT_EQ(sp_io_file_writer_from_path_at(&w, ut.file), SP_OK); sp_io_write(&w.base, "hello world", 11, SP_NULLPTR); sp_io_file_writer_close(&w); } sp_io_file_reader_t r = sp_zero; - ASSERT_EQ(sp_io_file_reader_from_path(&r, ut.file_path), SP_OK); + ASSERT_EQ(sp_io_file_reader_from_path_at(&r, ut.file), SP_OK); u64 discarded = 0; EXPECT_EQ(sp_io_discard(&r.base, 6, &discarded), SP_OK); diff --git a/test/io/file.c b/test/io/file.c index 046df888..9425fc03 100644 --- a/test/io/file.c +++ b/test/io/file.c @@ -9,16 +9,16 @@ typedef struct { io_step_t steps [IO_MAX_STEPS]; } io_file_reader_test_t; -void run_io_file_reader_test(int* utest_result, sp_str_t path, io_file_reader_test_t t) { +void run_io_file_reader_test(int* utest_result, sp_path_t path, io_file_reader_test_t t) { { sp_io_file_writer_t w = sp_zero; - sp_io_file_writer_from_path(&w, path); + sp_io_file_writer_from_path_at(&w, path); if (t.content) sp_io_write(&w.base, t.content, sp_cstr_len(t.content), SP_NULLPTR); sp_io_file_writer_close(&w); } sp_io_file_reader_t r = sp_zero; - sp_io_file_reader_from_path(&r, path); + sp_io_file_reader_from_path_at(&r, path); u8 wrapper_buf [64] = sp_zero; if (t.buffer) sp_io_reader_set_buffer(&r.base, wrapper_buf, t.buffer); @@ -62,7 +62,7 @@ void run_io_file_reader_test(int* utest_result, sp_str_t path, io_file_reader_te } UTEST_F(io, file_reader_read_full) { - run_io_file_reader_test(utest_result, ut.file_path, (io_file_reader_test_t){ + run_io_file_reader_test(utest_result, ut.file, (io_file_reader_test_t){ .content = "0123456789ABCDEF", .steps = { { .kind = IO_STEP_READ, .read = { 16, SP_OK, "0123456789ABCDEF" } }, @@ -71,7 +71,7 @@ UTEST_F(io, file_reader_read_full) { } UTEST_F(io, file_reader_eof_after_drain) { - run_io_file_reader_test(utest_result, ut.file_path, (io_file_reader_test_t){ + run_io_file_reader_test(utest_result, ut.file, (io_file_reader_test_t){ .content = "0123456789ABCDEF", .steps = { { .kind = IO_STEP_READ, .read = { 16, SP_OK, "0123456789ABCDEF" } }, @@ -81,7 +81,7 @@ UTEST_F(io, file_reader_eof_after_drain) { } UTEST_F(io, file_reader_eof_short) { - run_io_file_reader_test(utest_result, ut.file_path, (io_file_reader_test_t){ + run_io_file_reader_test(utest_result, ut.file, (io_file_reader_test_t){ .content = "short", .steps = { { .kind = IO_STEP_READ, .read = { 32, SP_OK, "short" } }, @@ -91,7 +91,7 @@ UTEST_F(io, file_reader_eof_short) { } UTEST_F(io, file_reader_eof_empty) { - run_io_file_reader_test(utest_result, ut.file_path, (io_file_reader_test_t){ + run_io_file_reader_test(utest_result, ut.file, (io_file_reader_test_t){ .content = "", .steps = { { .kind = IO_STEP_READ, .read = { 16, SP_ERR_IO_EOF } }, @@ -100,7 +100,7 @@ UTEST_F(io, file_reader_eof_empty) { } UTEST_F(io, file_reader_seek) { - run_io_file_reader_test(utest_result, ut.file_path, (io_file_reader_test_t){ + run_io_file_reader_test(utest_result, ut.file, (io_file_reader_test_t){ .content = "0123456789", .steps = { { .kind = IO_STEP_SEEK, .seek = { 5, SP_IO_SEEK_SET, SP_OK, 5 } }, @@ -110,7 +110,7 @@ UTEST_F(io, file_reader_seek) { } UTEST_F(io, file_reader_buffered_read) { - run_io_file_reader_test(utest_result, ut.file_path, (io_file_reader_test_t){ + run_io_file_reader_test(utest_result, ut.file, (io_file_reader_test_t){ .content = "0123456789ABCDEF", .buffer = 8, .steps = { @@ -120,7 +120,7 @@ UTEST_F(io, file_reader_buffered_read) { } UTEST_F(io, file_reader_buffered_seek_discards_buffer) { - run_io_file_reader_test(utest_result, ut.file_path, (io_file_reader_test_t){ + run_io_file_reader_test(utest_result, ut.file, (io_file_reader_test_t){ .content = "0123456789ABCDEF", .buffer = 8, .steps = { @@ -132,16 +132,16 @@ UTEST_F(io, file_reader_buffered_seek_discards_buffer) { } UTEST_F(io, file_reader_nonexistent) { - sp_str_t path = sp_test_file_path(&ut.file_manager, sp_str_lit("nonexistent.file")); + sp_path_t path = sp_path(ut.file.dir, sp_str_lit("nonexistent.file")); sp_io_file_reader_t r = sp_zero; - EXPECT_EQ(sp_io_file_reader_from_path(&r, path), SP_ERR_SYS_NOT_FOUND); + EXPECT_EQ(sp_io_file_reader_from_path_at(&r, path), SP_ERR_SYS_NOT_FOUND); sp_io_file_reader_close(&r); } UTEST_F(io, file_writer_nonexistent_dir) { - sp_str_t path = sp_test_file_path(&ut.file_manager, sp_str_lit("missing-dir/nested.file")); + sp_path_t path = sp_path(ut.file.dir, sp_str_lit("missing-dir/nested.file")); sp_io_file_writer_t w = sp_zero; - EXPECT_EQ(sp_io_file_writer_from_path(&w, path), SP_ERR_SYS_NOT_FOUND); + EXPECT_EQ(sp_io_file_writer_from_path_at(&w, path), SP_ERR_SYS_NOT_FOUND); } @@ -157,16 +157,16 @@ typedef struct { } expect; } io_file_writer_test_t; -void run_io_file_writer_test(int* utest_result, sp_mem_t mem, sp_str_t path, io_file_writer_test_t t) { +void run_io_file_writer_test(int* utest_result, sp_mem_t mem, sp_path_t path, io_file_writer_test_t t) { if (t.pre_content) { sp_io_file_writer_t w = sp_zero; - sp_io_file_writer_from_path(&w, path); + sp_io_file_writer_from_path_at(&w, path); sp_io_write(&w.base, t.pre_content, sp_cstr_len(t.pre_content), SP_NULLPTR); sp_io_file_writer_close(&w); } sp_io_file_writer_t w = sp_zero; - sp_io_file_writer_from_path(&w, path); + sp_io_file_writer_from_path_at(&w, path); u8 wrapper_buf [64] = sp_zero; if (t.buffer) sp_io_writer_set_buffer(&w.base, wrapper_buf, t.buffer); @@ -215,7 +215,7 @@ void run_io_file_writer_test(int* utest_result, sp_mem_t mem, sp_str_t path, io_ if (t.expect.content) { sp_str_t loaded = sp_zero; - sp_io_read_file(mem, path, &loaded); + sp_io_read_file_at(mem, path, &loaded); u64 n = sp_cstr_len(t.expect.content); EXPECT_EQ(loaded.len, n); sp_for(it, n) { @@ -225,7 +225,7 @@ void run_io_file_writer_test(int* utest_result, sp_mem_t mem, sp_str_t path, io_ } UTEST_F(io, file_writer_write) { - run_io_file_writer_test(utest_result, ut.mem, ut.file_path, (io_file_writer_test_t){ + run_io_file_writer_test(utest_result, ut.mem, ut.file, (io_file_writer_test_t){ .steps = { { .kind = IO_STEP_WRITE, .write = { "test data", SP_OK, 9 } }, }, @@ -234,7 +234,7 @@ UTEST_F(io, file_writer_write) { } UTEST_F(io, file_writer_overwrite) { - run_io_file_writer_test(utest_result, ut.mem, ut.file_path, (io_file_writer_test_t){ + run_io_file_writer_test(utest_result, ut.mem, ut.file, (io_file_writer_test_t){ .pre_content = "XXXXXXXX", .steps = { { .kind = IO_STEP_WRITE, .write = { "1234", SP_OK, 4 } }, @@ -244,7 +244,7 @@ UTEST_F(io, file_writer_overwrite) { } UTEST_F(io, file_writer_size) { - run_io_file_writer_test(utest_result, ut.mem, ut.file_path, (io_file_writer_test_t){ + run_io_file_writer_test(utest_result, ut.mem, ut.file, (io_file_writer_test_t){ .steps = { { .kind = IO_STEP_WRITE, .write = { "0123456789ABCDEF", SP_OK, 16 } }, { .kind = IO_STEP_SIZE, .size = { SP_OK, 16 } }, @@ -254,7 +254,7 @@ UTEST_F(io, file_writer_size) { } UTEST_F(io, file_writer_buffered_implicit_flush) { - run_io_file_writer_test(utest_result, ut.mem, ut.file_path, (io_file_writer_test_t){ + run_io_file_writer_test(utest_result, ut.mem, ut.file, (io_file_writer_test_t){ .buffer = 64, .steps = { { .kind = IO_STEP_WRITE, .write = { "hello", SP_OK, 5 } }, @@ -264,7 +264,7 @@ UTEST_F(io, file_writer_buffered_implicit_flush) { } UTEST_F(io, file_writer_buffered_larger_than_buffer) { - run_io_file_writer_test(utest_result, ut.mem, ut.file_path, (io_file_writer_test_t){ + run_io_file_writer_test(utest_result, ut.mem, ut.file, (io_file_writer_test_t){ .buffer = 4, .steps = { { .kind = IO_STEP_WRITE, .write = { "0123456789ABCDEF", SP_OK, 16 } }, @@ -283,14 +283,14 @@ UTEST_F(io, file_writer_buffered_larger_than_buffer) { // nul). Kept imperative. UTEST_F(io, file_writer_pad) { sp_io_file_writer_t w = sp_zero; - sp_io_file_writer_from_path(&w, ut.file_path); + sp_io_file_writer_from_path_at(&w, ut.file); sp_io_write(&w.base, "AA", 2, SP_NULLPTR); sp_io_pad(&w.base, 3, SP_NULLPTR); sp_io_write(&w.base, "BB", 2, SP_NULLPTR); sp_io_file_writer_close(&w); sp_io_file_reader_t r = sp_zero; - sp_io_file_reader_from_path(&r, ut.file_path); + sp_io_file_reader_from_path_at(&r, ut.file); u8 result [7] = sp_zero; u64 bytes = 0; EXPECT_EQ(sp_io_read(&r.base, result, 7, &bytes), SP_OK); @@ -309,12 +309,12 @@ UTEST_F(io, file_writer_pad) { // This is what the writer-side fast path keys off of. UTEST_F(io, file_reader_as_file) { sp_io_file_writer_t fw = sp_zero; - sp_io_file_writer_from_path(&fw, ut.file_path); + sp_io_file_writer_from_path_at(&fw, ut.file); sp_io_write(&fw.base, "x", 1, SP_NULLPTR); sp_io_file_writer_close(&fw); sp_io_file_reader_t r = sp_zero; - sp_io_file_reader_from_path(&r, ut.file_path); + sp_io_file_reader_from_path_at(&r, ut.file); EXPECT_TRUE(r.base.as_file != SP_NULLPTR); sp_sys_fd_t fd = SP_SYS_INVALID_FD; @@ -331,12 +331,12 @@ UTEST_F(io, file_reader_as_file) { // fd of unknown kind never claims it. UTEST_F(io, stream_reader_as_file) { sp_io_file_writer_t fw = sp_zero; - sp_io_file_writer_from_path(&fw, ut.file_path); + sp_io_file_writer_from_path_at(&fw, ut.file); sp_io_write(&fw.base, "x", 1, SP_NULLPTR); sp_io_file_writer_close(&fw); sp_sys_fd_t file = SP_SYS_INVALID_FD; - ASSERT_EQ(sp_sys_open_s(sp_sys_get_root(0), ut.file_path, SP_SYS_OPEN_MODE_RO, 0, &file), SP_OK); + ASSERT_EQ(sp_sys_open_s(ut.file.dir, ut.file.sub, SP_SYS_OPEN_MODE_RO, 0, &file), SP_OK); sp_io_stream_reader_t r = sp_zero; sp_io_stream_reader_from_file(&r, file, SP_IO_CLOSE_MODE_AUTO); @@ -366,16 +366,16 @@ UTEST_F(io, file_to_file_copy) { sp_for(i, sizeof(source)) source[i] = (u8)((i * 1103515245u + 12345u) >> 8); sp_io_file_writer_t sw = sp_zero; - sp_io_file_writer_from_path(&sw, ut.file_path); + sp_io_file_writer_from_path_at(&sw, ut.file); EXPECT_EQ(sp_io_write(&sw.base, source, sizeof(source), SP_NULLPTR), SP_OK); sp_io_file_writer_close(&sw); - sp_str_t dst_path = sp_test_file_path(&ut.file_manager, sp_str_lit("file_to_file_copy.dst")); + sp_path_t dst_path = sp_path(ut.file.dir, sp_str_lit("file_to_file_copy.dst")); sp_io_file_reader_t r = sp_zero; - sp_io_file_reader_from_path(&r, ut.file_path); + sp_io_file_reader_from_path_at(&r, ut.file); sp_io_file_writer_t w = sp_zero; - sp_io_file_writer_from_path(&w, dst_path); + sp_io_file_writer_from_path_at(&w, dst_path); u64 copied = 0; EXPECT_EQ(sp_io_copy(&w.base, &r.base, &copied), SP_OK); @@ -385,7 +385,7 @@ UTEST_F(io, file_to_file_copy) { sp_io_file_writer_close(&w); sp_str_t loaded = sp_zero; - sp_io_read_file(ut.mem, dst_path, &loaded); + sp_io_read_file_at(ut.mem, dst_path, &loaded); EXPECT_EQ(loaded.len, sizeof(source)); sp_for(i, sizeof(source)) EXPECT_EQ((u8)loaded.data[i], source[i]); } @@ -398,18 +398,18 @@ UTEST_F(io, stream_file_to_file_copy) { sp_for(i, sizeof(source)) source[i] = (u8)((i * 1103515245u + 12345u) >> 8); sp_io_file_writer_t sw = sp_zero; - sp_io_file_writer_from_path(&sw, ut.file_path); + sp_io_file_writer_from_path_at(&sw, ut.file); EXPECT_EQ(sp_io_write(&sw.base, source, sizeof(source), SP_NULLPTR), SP_OK); sp_io_file_writer_close(&sw); - sp_str_t dst_path = sp_test_file_path(&ut.file_manager, sp_str_lit("stream_file_to_file_copy.dst")); + sp_path_t dst_path = sp_path(ut.file.dir, sp_str_lit("stream_file_to_file_copy.dst")); sp_sys_fd_t file = SP_SYS_INVALID_FD; - ASSERT_EQ(sp_sys_open_s(sp_sys_get_root(0), ut.file_path, SP_SYS_OPEN_MODE_RO, 0, &file), SP_OK); + ASSERT_EQ(sp_sys_open_s(ut.file.dir, ut.file.sub, SP_SYS_OPEN_MODE_RO, 0, &file), SP_OK); sp_io_stream_reader_t r = sp_zero; sp_io_stream_reader_from_file(&r, file, SP_IO_CLOSE_MODE_AUTO); sp_io_file_writer_t w = sp_zero; - sp_io_file_writer_from_path(&w, dst_path); + sp_io_file_writer_from_path_at(&w, dst_path); u64 copied = 0; EXPECT_EQ(sp_io_copy(&w.base, &r.base, &copied), SP_OK); @@ -419,7 +419,7 @@ UTEST_F(io, stream_file_to_file_copy) { sp_io_file_writer_close(&w); sp_str_t loaded = sp_zero; - sp_io_read_file(ut.mem, dst_path, &loaded); + sp_io_read_file_at(ut.mem, dst_path, &loaded); EXPECT_EQ(loaded.len, sizeof(source)); sp_for(i, sizeof(source)) EXPECT_EQ((u8)loaded.data[i], source[i]); } @@ -435,7 +435,7 @@ UTEST_F(io, file_copy_fast_path_falls_back_for_mem_source) { EXPECT_TRUE(r.as_file == SP_NULLPTR); // Mem reader has no file to hand out. sp_io_file_writer_t w = sp_zero; - sp_io_file_writer_from_path(&w, ut.file_path); + sp_io_file_writer_from_path_at(&w, ut.file); u64 copied = 0; EXPECT_EQ(sp_io_copy(&w.base, &r, &copied), SP_OK); @@ -444,7 +444,7 @@ UTEST_F(io, file_copy_fast_path_falls_back_for_mem_source) { sp_io_file_writer_close(&w); sp_str_t loaded = sp_zero; - sp_io_read_file(ut.mem, ut.file_path, &loaded); + sp_io_read_file_at(ut.mem, ut.file, &loaded); EXPECT_EQ(loaded.len, n); sp_for(i, n) EXPECT_EQ(loaded.data[i], content[i]); } @@ -457,13 +457,13 @@ UTEST_F(io, read_file_larger_than_single_read) { const u64 size = (u64)0x7ffff000 + 1; sp_io_file_writer_t w = sp_zero; - ASSERT_EQ(sp_io_file_writer_from_path(&w, ut.file_path), SP_OK); + ASSERT_EQ(sp_io_file_writer_from_path_at(&w, ut.file), SP_OK); ASSERT_EQ(sp_io_file_writer_seek(&w, (s64)(size - 1), SP_IO_SEEK_SET, SP_NULLPTR), SP_OK); ASSERT_EQ(sp_io_write(&w.base, "!", 1, SP_NULLPTR), SP_OK); sp_io_file_writer_close(&w); sp_str_t loaded = sp_zero; - EXPECT_EQ(sp_io_read_file(ut.mem, ut.file_path, &loaded), SP_OK); + EXPECT_EQ(sp_io_read_file_at(ut.mem, ut.file, &loaded), SP_OK); EXPECT_EQ(loaded.len, (u32)size); if (loaded.len == (u32)size) { EXPECT_EQ(loaded.data[size - 1], '!'); @@ -481,12 +481,12 @@ UTEST_F(io, read_file_larger_than_single_read) { // for the size, and the answer is then memoized. UTEST_F(io, file_reader_from_file_defers_stat) { sp_io_file_writer_t w = sp_zero; - sp_io_file_writer_from_path(&w, ut.file_path); + sp_io_file_writer_from_path_at(&w, ut.file); sp_io_write(&w.base, "abc", 3, SP_NULLPTR); sp_io_file_writer_close(&w); sp_sys_fd_t fd = SP_SYS_INVALID_FD; - ASSERT_EQ(sp_sys_open_s(sp_sys_get_root(0), ut.file_path, SP_SYS_OPEN_MODE_RO, 0, &fd), SP_OK); + ASSERT_EQ(sp_sys_open_s(ut.file.dir, ut.file.sub, SP_SYS_OPEN_MODE_RO, 0, &fd), SP_OK); sp_io_file_reader_t r = sp_zero; EXPECT_EQ(sp_io_file_reader_from_file(&r, fd, SP_IO_CLOSE_MODE_AUTO), SP_OK); @@ -501,12 +501,12 @@ UTEST_F(io, file_reader_from_file_defers_stat) { UTEST_F(io, file_writer_from_fd_defers_stat) { sp_io_file_writer_t w = sp_zero; - sp_io_file_writer_from_path(&w, ut.file_path); + sp_io_file_writer_from_path_at(&w, ut.file); sp_io_write(&w.base, "abc", 3, SP_NULLPTR); sp_io_file_writer_close(&w); sp_sys_fd_t fd = SP_SYS_INVALID_FD; - ASSERT_EQ(sp_sys_open_s(sp_sys_get_root(0), ut.file_path, SP_SYS_OPEN_MODE_WO, 0, &fd), SP_OK); + ASSERT_EQ(sp_sys_open_s(ut.file.dir, ut.file.sub, SP_SYS_OPEN_MODE_WO, 0, &fd), SP_OK); sp_io_file_writer_t a = sp_zero; EXPECT_EQ(sp_io_file_writer_from_fd(&a, fd, SP_IO_CLOSE_MODE_AUTO), SP_OK); @@ -524,12 +524,12 @@ UTEST_F(io, file_writer_from_fd_defers_stat) { // size_force is the explicit refresh. UTEST_F(io, file_reader_size_is_memoized) { sp_io_file_writer_t w = sp_zero; - sp_io_file_writer_from_path(&w, ut.file_path); + sp_io_file_writer_from_path_at(&w, ut.file); sp_io_write(&w.base, "abc", 3, SP_NULLPTR); sp_io_flush(&w.base); sp_io_file_reader_t r = sp_zero; - sp_io_file_reader_from_path(&r, ut.file_path); + sp_io_file_reader_from_path_at(&r, ut.file); EXPECT_FALSE(r.size_known); u64 size = 0; @@ -553,7 +553,7 @@ UTEST_F(io, file_reader_size_is_memoized) { // asking; writes then keep the size current. UTEST_F(io, file_writer_from_path_knows_size) { sp_io_file_writer_t w = sp_zero; - sp_io_file_writer_from_path(&w, ut.file_path); + sp_io_file_writer_from_path_at(&w, ut.file); EXPECT_TRUE(w.size_known); EXPECT_EQ(w.size, (u64)0); @@ -572,21 +572,23 @@ UTEST_F(io, file_writer_from_path_knows_size) { UTEST_F(io, file_reader_positional_does_not_touch_kernel_cursor) { SKIP_ON_WIN32() sp_io_file_writer_t w = sp_zero; - sp_io_file_writer_from_path(&w, ut.file_path); + sp_io_file_writer_from_path_at(&w, ut.file); sp_io_write(&w.base, "0123456789ABCDEF", 16, SP_NULLPTR); sp_io_file_writer_close(&w); sp_sys_fd_t fd = SP_SYS_INVALID_FD; - sp_sys_open_s(sp_sys_get_root(0), ut.file_path, SP_SYS_OPEN_MODE_RO, 0, &fd); + sp_sys_open_s(ut.file.dir, ut.file.sub, SP_SYS_OPEN_MODE_RO, 0, &fd); const s64 parked = 7; - sp_sys_lseek(fd, parked, SP_IO_SEEK_SET); + sp_sys_lseek(fd, parked, SP_IO_SEEK_SET, SP_NULLPTR); sp_io_file_reader_t r = sp_zero; sp_io_file_reader_from_file(&r, fd, SP_IO_CLOSE_MODE_NONE); u8 buf [4] = sp_zero; sp_io_read(&r.base, buf, 4, SP_NULLPTR); - EXPECT_EQ(sp_sys_lseek(fd, 0, SP_IO_SEEK_CUR), parked); + s64 position = 0; + sp_sys_lseek(fd, 0, SP_IO_SEEK_CUR, &position); + EXPECT_EQ(position, parked); sp_io_file_reader_close(&r); sp_sys_close(fd); @@ -599,7 +601,7 @@ UTEST_F(io, file_seek_beyond_4gb) { u8 marker [4] = {0xDE, 0xAD, 0xBE, 0xEF}; sp_io_file_writer_t w = sp_zero; - sp_io_file_writer_from_path(&w, ut.file_path); + sp_io_file_writer_from_path_at(&w, ut.file); s64 seek_pos = 0; EXPECT_EQ(sp_io_file_writer_seek(&w, offset, SP_IO_SEEK_SET, &seek_pos), SP_OK); EXPECT_EQ(seek_pos, offset); @@ -607,7 +609,7 @@ UTEST_F(io, file_seek_beyond_4gb) { sp_io_file_writer_close(&w); sp_io_file_reader_t r = sp_zero; - sp_io_file_reader_from_path(&r, ut.file_path); + sp_io_file_reader_from_path_at(&r, ut.file); u64 end = 0; EXPECT_EQ(sp_io_file_reader_size(&r, &end), SP_OK); EXPECT_EQ(end, (u64)offset + 4); diff --git a/test/io/io.h b/test/io/io.h index 1673d363..b7b9a362 100644 --- a/test/io/io.h +++ b/test/io/io.h @@ -7,7 +7,7 @@ struct io { - sp_str_t file_path; + sp_path_t file; sp_test_file_manager_t file_manager; sp_mem_arena_t* arena; sp_mem_t mem; @@ -18,11 +18,14 @@ UTEST_F_SETUP(io) { ut.arena = sp_mem_arena_new(sp_mem_os_new()); ut.mem = sp_mem_arena_as_allocator(ut.arena); sp_test_file_manager_init(&ut.file_manager); - ut.file_path = sp_test_file_create_empty(&ut.file_manager, sp_str_lit("sp_io_rw.file")); + ut.file.sub = sp_str_lit("sp_io_rw.file"); + sp_test_file_create_empty(&ut.file_manager, ut.file.sub); + sp_sys_open_dir_s(sp_sys_get_root(0), ut.file_manager.paths.test, 0, &ut.file.dir); } UTEST_F_TEARDOWN(io) { SKIP_ON_WASM() + sp_sys_close(ut.file.dir); sp_test_file_manager_cleanup(&ut.file_manager); sp_mem_arena_destroy(ut.arena); } diff --git a/test/io/seeking_reader.c b/test/io/seeking_reader.c index a42525df..9f487045 100644 --- a/test/io/seeking_reader.c +++ b/test/io/seeking_reader.c @@ -99,16 +99,16 @@ typedef struct { io_step_t steps [IO_MAX_STEPS]; } io_seeking_reader_file_test_t; -void run_io_seeking_reader_file_test(int* utest_result, sp_str_t path, io_seeking_reader_file_test_t t) { +void run_io_seeking_reader_file_test(int* utest_result, sp_path_t path, io_seeking_reader_file_test_t t) { { sp_io_file_writer_t w = sp_zero; - sp_io_file_writer_from_path(&w, path); + sp_io_file_writer_from_path_at(&w, path); if (t.content) sp_io_write(&w.base, t.content, sp_cstr_len(t.content), SP_NULLPTR); sp_io_file_writer_close(&w); } sp_io_file_reader_t fr = sp_zero; - sp_io_file_reader_from_path(&fr, path); + sp_io_file_reader_from_path_at(&fr, path); u8 wrapper_buf [64] = sp_zero; if (t.buffer) sp_io_reader_set_buffer(&fr.base, wrapper_buf, t.buffer); @@ -155,7 +155,7 @@ void run_io_seeking_reader_file_test(int* utest_result, sp_str_t path, io_seekin } UTEST_F(io, seeking_reader_file_seek) { - run_io_seeking_reader_file_test(utest_result, ut.file_path, (io_seeking_reader_file_test_t){ + run_io_seeking_reader_file_test(utest_result, ut.file, (io_seeking_reader_file_test_t){ .content = "0123456789ABCDEF", .steps = { { .kind = IO_STEP_SEEK, .seek = { 5, SP_IO_SEEK_SET, SP_OK, 5 } }, @@ -165,7 +165,7 @@ UTEST_F(io, seeking_reader_file_seek) { } UTEST_F(io, seeking_reader_file_seek_whence) { - run_io_seeking_reader_file_test(utest_result, ut.file_path, (io_seeking_reader_file_test_t){ + run_io_seeking_reader_file_test(utest_result, ut.file, (io_seeking_reader_file_test_t){ .content = "0123456789ABCDEF", .steps = { { .kind = IO_STEP_SEEK, .seek = { 4, SP_IO_SEEK_SET, SP_OK, 4 } }, @@ -179,7 +179,7 @@ UTEST_F(io, seeking_reader_file_seek_whence) { } UTEST_F(io, seeking_reader_file_seek_invalid) { - run_io_seeking_reader_file_test(utest_result, ut.file_path, (io_seeking_reader_file_test_t){ + run_io_seeking_reader_file_test(utest_result, ut.file, (io_seeking_reader_file_test_t){ .content = "0123456789", .steps = { { .kind = IO_STEP_SEEK, .seek = { -10, SP_IO_SEEK_SET, SP_ERR_IO_SEEK_INVALID, -1 } }, @@ -188,7 +188,7 @@ UTEST_F(io, seeking_reader_file_seek_invalid) { } UTEST_F(io, seeking_reader_file_seek_buffered) { - run_io_seeking_reader_file_test(utest_result, ut.file_path, (io_seeking_reader_file_test_t){ + run_io_seeking_reader_file_test(utest_result, ut.file, (io_seeking_reader_file_test_t){ .content = "0123456789ABCDEF", .buffer = 8, .steps = { diff --git a/test/leak.c b/test/leak.c index 06bfc8ec..f8c02e0a 100644 --- a/test/leak.c +++ b/test/leak.c @@ -166,8 +166,9 @@ UTEST(tracking, realloc_zero_is_free) { // SP_PS LEAK // ////////////// static sp_str_t leak_ps_get_process_path(sp_mem_t mem) { - sp_str_t exe = sp_fs_parent_path(sp_fs_get_exe_path(mem)); - sp_str_t process = sp_fs_join_path(mem, exe, sp_str_lit("process")); + sp_str_t exe = sp_zero; + sp_fs_get_exe_path(mem, &exe); + sp_str_t process = sp_fs_join_path(mem, sp_fs_parent_path(exe), sp_str_lit("process")); return sp_fs_replace_ext(mem, process, sp_os_get_executable_ext()); } diff --git a/test/ps.c b/test/ps.c index 8638771a..10eb6627 100644 --- a/test/ps.c +++ b/test/ps.c @@ -53,15 +53,17 @@ typedef struct { // FIXTURES // ////////////// sp_str_t get_process_path(sp_mem_t mem) { - sp_str_t exe = sp_fs_parent_path(sp_fs_get_exe_path(mem)); - sp_str_t process = sp_fs_join_path(mem, exe, sp_str_lit("process")); + sp_str_t exe = sp_zero; + sp_fs_get_exe_path(mem, &exe); + sp_str_t process = sp_fs_join_path(mem, sp_fs_parent_path(exe), sp_str_lit("process")); process = sp_fs_replace_ext(mem, process, sp_os_get_executable_ext()); return process; } const c8* get_process_path_c(sp_mem_t mem) { - sp_str_t exe = sp_fs_parent_path(sp_fs_get_exe_path(mem)); - sp_str_t process = sp_fs_join_path(mem, exe, sp_str_lit("process")); + sp_str_t exe = sp_zero; + sp_fs_get_exe_path(mem, &exe); + sp_str_t process = sp_fs_join_path(mem, sp_fs_parent_path(exe), sp_str_lit("process")); process = sp_fs_replace_ext(mem, process, sp_os_get_executable_ext()); return sp_str_to_cstr(mem, process); } @@ -311,7 +313,7 @@ UTEST_F(ps, io_create_file_null) { }, .fn = TEST_PROC_FUNCTION_ECHO, }); - sp_sys_lseek(fd, 0, SP_SEEK_SET); + sp_sys_lseek(fd, 0, SP_SEEK_SET, SP_NULLPTR); u64 bytes_read = 0; sp_sys_read(fd, ut.buffer.data, ut.buffer.len, &bytes_read); @@ -327,7 +329,7 @@ UTEST_F(ps, io_file_create_null) { sp_sys_fd_t fd = SP_SYS_INVALID_FD; sp_sys_open_s(sp_sys_get_root(0), file_path, SP_SYS_OPEN_MODE_RW, SP_SYS_OPEN_CREATE, &fd); sp_sys_write(fd, sp_test_ps_canary.data, sp_test_ps_canary.len, SP_NULLPTR); - sp_sys_lseek(fd, 0, SP_SEEK_SET); + sp_sys_lseek(fd, 0, SP_SEEK_SET, SP_NULLPTR); sp_test_proc_io(&ut, &ur, (sp_test_proc_io_config_t) { .io = { @@ -366,7 +368,7 @@ UTEST_F(ps, io_create_null_file) { }, .fn = TEST_PROC_FUNCTION_ECHO, }); - sp_sys_lseek(fd, 0, SP_SEEK_SET); + sp_sys_lseek(fd, 0, SP_SEEK_SET, SP_NULLPTR); u64 bytes_read = 0; sp_sys_read(fd, ut.buffer.data, ut.buffer.len, &bytes_read); @@ -382,7 +384,7 @@ UTEST_F(ps, io_file_null_file) { sp_sys_fd_t in_fd = SP_SYS_INVALID_FD; sp_sys_open_s(sp_sys_get_root(0), in_path, SP_SYS_OPEN_MODE_RW, SP_SYS_OPEN_CREATE, &in_fd); sp_sys_write(in_fd, sp_test_ps_canary.data, sp_test_ps_canary.len, SP_NULLPTR); - sp_sys_lseek(in_fd, 0, SP_SEEK_SET); + sp_sys_lseek(in_fd, 0, SP_SEEK_SET, SP_NULLPTR); sp_str_t err_path = sp_test_file_create_empty(&ut.file_manager, sp_str_lit("stderr.file")); sp_sys_fd_t err_fd = SP_SYS_INVALID_FD; @@ -401,7 +403,7 @@ UTEST_F(ps, io_file_null_file) { }, .fn = TEST_PROC_FUNCTION_ECHO, }); - sp_sys_lseek(err_fd, 0, SP_SEEK_SET); + sp_sys_lseek(err_fd, 0, SP_SEEK_SET, SP_NULLPTR); u64 bytes_read = 0; sp_sys_read(err_fd, ut.buffer.data, ut.buffer.len, &bytes_read); diff --git a/test/sys/fd.c b/test/sys/fd.c index eabe93aa..026e0f17 100644 --- a/test/sys/fd.c +++ b/test/sys/fd.c @@ -32,7 +32,7 @@ static sp_err_t run(sp_test_t* t, test_t* c) { case OP_WRITE: err = sp_sys_write(GARBAGE_FD, "A", 1, &n); break; case OP_OPEN_DIR: { sp_sys_fd_t out = SP_SYS_INVALID_FD; - err = sp_sys_open_dir_s(GARBAGE_FD, sp_str_lit("A"), &out); + err = sp_sys_open_dir_s(GARBAGE_FD, sp_str_lit("A"), 0, &out); if (!err) sp_sys_close(out); break; } diff --git a/test/sys/fd_path.c b/test/sys/fd_path.c new file mode 100644 index 00000000..37baa3b4 --- /dev/null +++ b/test/sys/fd_path.c @@ -0,0 +1,262 @@ +#include "sp.h" +#include "sp/sp_test.h" + +typedef enum { + TARGET_FILE, + TARGET_DIR, + TARGET_CWD, + TARGET_PIPE, + TARGET_GARBAGE, +} target_t; + +typedef enum { + FATE_KEPT, + FATE_UNLINKED, + FATE_REMOVED, + FATE_RENAMED, + FATE_REPLACED, +} fate_t; + +typedef struct { + sp_err_t err; + const c8* path; +} expect_t; + +typedef struct { + const c8* name; + target_t target; + const c8* path; + const c8* alias; + const c8* decoy; + fate_t fate; + expect_t expect; +} test_t; + +static const test_t tests [] = { + { + .name = "file", + .target = TARGET_FILE, + .path = "A", + .expect = { .path = "A" }, + }, + { + .name = "dir", + .target = TARGET_DIR, + .path = "A", + .expect = { .path = "A" }, + }, + { + .name = "cwd", + .target = TARGET_CWD, + .expect = { .path = "." }, + }, + { + .name = "deleted_suffix_is_a_name", + .target = TARGET_FILE, + .path = "A (deleted)", + .expect = { .path = "A (deleted)" }, + }, + { + .name = "renamed", + .target = TARGET_FILE, + .path = "A", + .fate = FATE_RENAMED, + .expect = { .path = "B" }, + }, + { + .name = "unlinked", + .target = TARGET_FILE, + .path = "A", + .fate = FATE_UNLINKED, + .expect = { .err = SP_ERR_SYS_NOT_FOUND }, + }, + { + .name = "unlinked_with_a_live_alias", + .target = TARGET_FILE, + .path = "A", + .alias = "B", + .fate = FATE_UNLINKED, + .expect = { .err = SP_ERR_SYS_NOT_FOUND }, + }, + { + .name = "unlinked_beside_a_deleted_suffix", + .target = TARGET_FILE, + .path = "A", + .decoy = "A (deleted)", + .fate = FATE_UNLINKED, + .expect = { .err = SP_ERR_SYS_NOT_FOUND }, + }, + { + .name = "replaced", + .target = TARGET_FILE, + .path = "A", + .fate = FATE_REPLACED, + .expect = { .err = SP_ERR_SYS_NOT_FOUND }, + }, + { + .name = "removed_dir", + .target = TARGET_DIR, + .path = "A", + .fate = FATE_REMOVED, + .expect = { .err = SP_ERR_SYS_NOT_FOUND }, + }, + { + .name = "pipe", + .target = TARGET_PIPE, + .expect = { .err = SP_ERR_SYS_NOT_FOUND }, + }, + { + .name = "garbage", + .target = TARGET_GARBAGE, + .expect = { .err = SP_ERR_SYS_BAD_FD }, + }, +}; + +#define GARBAGE_FD ((sp_sys_fd_t)0x55555) + +static bool same_file(sp_path_t a, sp_path_t b) { + sp_sys_file_meta_t ma = sp_zero; + sp_sys_file_meta_t mb = sp_zero; + if (sp_sys_get_link_metadata_s(a.dir, a.sub, &ma)) return false; + if (sp_sys_get_link_metadata_s(b.dir, b.sub, &mb)) return false; + return ma.id == mb.id && ma.device == mb.device; +} + +static sp_err_t run(sp_test_t* t, test_t* c) { + sp_test_skip_on_wasm() + + sp_mem_t mem = sp_test_arena(t); + sp_path_t sandbox = sp_test_dir(t); + sp_path_t path = sp_path_join(mem, sandbox, sp_cstr_as_str(c->path)); + sp_path_t alias = sp_path_join(mem, sandbox, sp_cstr_as_str(c->alias)); + sp_path_t decoy = sp_path_join(mem, sandbox, sp_cstr_as_str(c->decoy)); + sp_path_t other = sp_path_join(mem, sandbox, sp_str_lit("B")); + sp_path_t expect = sp_path_join(mem, sandbox, sp_cstr_as_str(c->expect.path)); + + sp_sys_pipe_t pipe = sp_zero; + sp_sys_fd_t fd = SP_SYS_INVALID_FD; + switch (c->target) { + case TARGET_FILE: { + sp_must_ok(t, sp_fs_create_file_at(path)); + if (c->alias) sp_must_ok(t, sp_fs_create_hard_link_at(path, alias)); + if (c->decoy) sp_must_ok(t, sp_fs_create_file_at(decoy)); + sp_must_ok(t, sp_sys_open_s(path.dir, path.sub, SP_SYS_OPEN_MODE_RO, 0, &fd)); + break; + } + case TARGET_DIR: { + sp_must_ok(t, sp_fs_create_dir_at(path)); + sp_must_ok(t, sp_sys_open_dir_s(path.dir, path.sub, 0, &fd)); + break; + } + case TARGET_CWD: { + expect = sp_path_cwd(sp_cstr_as_str(c->expect.path)); + fd = expect.dir; + break; + } + case TARGET_PIPE: { + sp_must_ok(t, sp_sys_pipe(&pipe, sp_zero_s(sp_sys_pipe_desc_t))); + fd = pipe.r; + break; + } + case TARGET_GARBAGE: { + fd = GARBAGE_FD; + break; + } + } + + switch (c->fate) { + case FATE_KEPT: { + break; + } + case FATE_UNLINKED: { + sp_must_ok(t, sp_sys_unlink_s(path.dir, path.sub)); + break; + } + case FATE_REMOVED: { + sp_must_ok(t, sp_sys_rmdir_s(path.dir, path.sub)); + break; + } + case FATE_RENAMED: { + sp_must_ok(t, sp_sys_rename_s(path.dir, path.sub, other.dir, other.sub)); + break; + } + case FATE_REPLACED: { + sp_must_ok(t, sp_sys_unlink_s(path.dir, path.sub)); + sp_must_ok(t, sp_fs_create_file_at(path)); + break; + } + } + + c8 buf [SP_PATH_MAX]; + sp_for(it, sizeof(buf)) buf[it] = (c8)0xAB; + u64 len = 0; + sp_err_t err = sp_sys_get_fd_path(fd, buf, sizeof(buf), &len); + + sp_expect_err_eq(t, err, c->expect.err); + if (!err) { + sp_expect_eq(t, buf[len], 0); + sp_expect(t, same_file(expect, sp_path_cwd(sp_str(buf, (u32)len)))); + } + + switch (c->target) { + case TARGET_FILE: + case TARGET_DIR: { + sp_sys_close(fd); + break; + } + case TARGET_PIPE: { + sp_sys_close(pipe.r); + sp_sys_close(pipe.w); + break; + } + case TARGET_CWD: + case TARGET_GARBAGE: { + break; + } + } + return SP_OK; +} + +sp_test_each_fn(sys, fd_path, test_t, tests, run); + +typedef struct { + const c8* name; + s32 room; + sp_err_t err; +} room_t; + +static const room_t rooms [] = { + { .name = "fits_with_nul", .room = 1 }, + { .name = "no_room_for_nul", .room = 0, .err = SP_ERR_SYS_NAME_TOO_LONG }, + { .name = "too_small", .room = -1, .err = SP_ERR_SYS_NAME_TOO_LONG }, +}; + +sp_test_each(sys, fd_path_room, room_t, rooms) { + sp_test_skip_on_wasm() + + sp_path_t sandbox = sp_test_dir(t); + sp_sys_fd_t dir = SP_SYS_INVALID_FD; + sp_must_ok(t, sp_sys_open_dir_s(sandbox.dir, sandbox.sub, 0, &dir)); + + c8 full [SP_PATH_MAX]; + u64 len = 0; + sp_must_ok(t, sp_sys_get_fd_path(dir, full, sizeof(full), &len)); + + c8 buf [SP_PATH_MAX]; + sp_for(at, sizeof(buf)) buf[at] = (c8)0xAB; + u64 n = 1; + sp_err_t err = sp_sys_get_fd_path(dir, buf, (u64)((s64)len + it->room), &n); + + sp_expect_err_eq(t, err, it->err); + if (err) { + sp_expect_eq(t, n, 0); + } + else { + sp_expect_eq(t, n, len); + sp_expect_eq(t, buf[len], 0); + sp_expect_mem_eq(t, buf, full, len); + } + + sp_sys_close(dir); + return SP_OK; +} diff --git a/test/sys/harness.h b/test/sys/harness.h index ec61982f..3012eb7e 100644 --- a/test/sys/harness.h +++ b/test/sys/harness.h @@ -15,6 +15,7 @@ typedef enum { SYS_SETUP_FILE, SYS_SETUP_DIR, SYS_SETUP_SYMLINK, + SYS_SETUP_DIR_SYMLINK, } sys_setup_kind_t; typedef struct { @@ -39,7 +40,7 @@ typedef struct { sys_step_kind_t kind; union { struct { u32 slot; const c8* path; sp_sys_open_mode_t mode; u32 flags; sp_err_t err; } open; - struct { u32 slot; const c8* path; sp_err_t err; } open_dir; + struct { u32 slot; const c8* path; u32 flags; sp_err_t err; } open_dir; struct { u32 slot; u64 count; const c8* expect; sp_err_t err; } read; struct { u32 slot; const c8* data; sp_err_t err; } write; struct { u32 slot; const c8* data; u64 offset; sp_err_t err; } pwrite; @@ -59,8 +60,6 @@ typedef struct { sys_expect_t expect [SYS_CASE_MAX_EXPECT]; } sys_case_t; -static sp_test_once_t sys_symlink_probe = sp_zero; - sp_err_t sys_case_run(sp_test_t* t, sys_case_t* c); #endif diff --git a/test/sys/inherit.c b/test/sys/inherit.c index abcaf0c9..e1acdbd1 100644 --- a/test/sys/inherit.c +++ b/test/sys/inherit.c @@ -42,8 +42,10 @@ static const c8* inherited_name(s64 value) { static s32 spawn_probe(sp_test_t* t, const c8* op, s64 handle) { sp_mem_t mem = sp_test_arena(t); + sp_str_t exe = sp_zero; + sp_expect_ok(t, sp_fs_get_exe_path(mem, &exe)); sp_ps_output_t out = sp_ps_run(mem, (sp_ps_config_t) { - .command = sp_fs_get_exe_path(mem), + .command = exe, .args = { sp_str_lit("probe"), sp_cstr_as_str(op), diff --git a/test/sys/iter.c b/test/sys/iter.c deleted file mode 100644 index bc12e304..00000000 --- a/test/sys/iter.c +++ /dev/null @@ -1,143 +0,0 @@ -#include "harness.h" - -#if !defined(SP_WASM) - -#define SYS_ITER_MAX_RAW 8 -#define SYS_ITER_BUF_SIZE 4096 - -typedef struct { - const c8* name; - sp_fs_kind_t kind; -} sys_iter_raw_t; - -typedef struct { - const c8* name; - sys_iter_raw_t raw [SYS_ITER_MAX_RAW]; -} sys_iter_case_t; - -#if defined(SP_WIN32) -static u64 sys_iter_pack(u8* buf, const sys_iter_raw_t* raw) { - u64 cursor = 0; - u64 len = 0; - sp_for(it, SYS_ITER_MAX_RAW) { - if (!raw[it].name) break; - u32 name_len = sp_cstr_len(raw[it].name); - - sp_nt_file_directory_information_t* d = sp_ptr_cast(sp_nt_file_directory_information_t*, buf + cursor); - *d = sp_zero_s(sp_nt_file_directory_information_t); - d->FileNameLength = name_len * sizeof(u16); - sp_for(n, name_len) { - d->FileName[n] = (u16)raw[it].name[n]; - } - switch (raw[it].kind) { - case SP_FS_KIND_FILE: { d->FileAttributes = FILE_ATTRIBUTE_NORMAL; break; } - case SP_FS_KIND_DIR: { d->FileAttributes = FILE_ATTRIBUTE_DIRECTORY; break; } - case SP_FS_KIND_SYMLINK: { d->FileAttributes = FILE_ATTRIBUTE_REPARSE_POINT; break; } - case SP_FS_KIND_NONE: { break; } - } - - len = cursor + sizeof(sp_nt_file_directory_information_t) + d->FileNameLength; - bool last = it + 1 >= SYS_ITER_MAX_RAW || !raw[it + 1].name; - d->NextEntryOffset = last ? 0 : (u32)sp_align_offset(sizeof(sp_nt_file_directory_information_t) + d->FileNameLength, 8); - cursor += d->NextEntryOffset; - } - return len; -} -#elif defined(SP_LINUX) -static u64 sys_iter_pack(u8* buf, const sys_iter_raw_t* raw) { - u64 cursor = 0; - sp_for(it, SYS_ITER_MAX_RAW) { - if (!raw[it].name) break; - u32 name_len = sp_cstr_len(raw[it].name); - - sp_sys_dirent64_t* d = sp_ptr_cast(sp_sys_dirent64_t*, buf + cursor); - d->d_ino = it + 1; - d->d_off = 0; - d->d_reclen = (u16)sp_align_offset(sizeof(sp_sys_dirent64_t) + name_len + 1, 8); - switch (raw[it].kind) { - case SP_FS_KIND_FILE: { d->d_type = SP_DT_REG; break; } - case SP_FS_KIND_DIR: { d->d_type = SP_DT_DIR; break; } - case SP_FS_KIND_SYMLINK: { d->d_type = SP_DT_LNK; break; } - case SP_FS_KIND_NONE: { d->d_type = 0; break; } - } - sp_mem_copy(d->d_name, raw[it].name, name_len + 1); - cursor += d->d_reclen; - } - return cursor; -} -#elif defined(SP_MACOS) || defined(SP_COSMO) -static u64 sys_iter_pack(u8* buf, const sys_iter_raw_t* raw) { - u64 cursor = 0; - sp_for(it, SYS_ITER_MAX_RAW) { - if (!raw[it].name) break; - u32 name_len = sp_cstr_len(raw[it].name); - - struct dirent d = sp_zero; - d.d_reclen = sizeof(struct dirent); - switch (raw[it].kind) { - case SP_FS_KIND_FILE: { d.d_type = SP_DT_REG; break; } - case SP_FS_KIND_DIR: { d.d_type = SP_DT_DIR; break; } - case SP_FS_KIND_SYMLINK: { d.d_type = SP_DT_LNK; break; } - case SP_FS_KIND_NONE: { d.d_type = 0; break; } - } - sp_mem_copy(d.d_name, raw[it].name, name_len + 1); - sp_mem_copy(buf + cursor, &d, sizeof(struct dirent)); - cursor += sizeof(struct dirent); - } - return cursor; -} -#endif - -static const sys_iter_case_t sys_iter_cases [] = { - { - .name = "decodes_kinds", - .raw = { - { "A", SP_FS_KIND_FILE }, - { "B", SP_FS_KIND_DIR }, - { "L", SP_FS_KIND_SYMLINK }, - }, - }, - { - .name = "returns_dot_entries", - .raw = { - { ".", SP_FS_KIND_DIR }, - { "..", SP_FS_KIND_DIR }, - { "A", SP_FS_KIND_FILE }, - }, - }, -}; - -sp_test_each(sys, iter, sys_iter_case_t, sys_iter_cases) { - SP_ALIGNED u8 raw [SYS_ITER_BUF_SIZE]; - sp_mem_buffer_t buf = { - .data = raw, - .capacity = sizeof(raw), - }; - buf.len = sys_iter_pack(raw, it->raw); - - sp_sys_dir_t dir = sp_zero; - u64 cursor = 0; - - sp_carr_for(it->raw, n) { - if (!it->raw[n].name) break; - - if (cursor >= buf.len) { - sp_test_fail(t, "parse consumed the buffer before {}", sp_fmt_cstr(it->raw[n].name)); - return SP_ERR; - } - - sp_test_kv_c(t, "entry", it->raw[n].name); - - sp_sys_dir_entry_t entry = sp_zero; - sp_must_ok(t, sp_sys_dir_parse(&dir, &buf, &cursor, &entry)); - - sp_expect_str_eq(t, sp_str(entry.name, entry.len), sp_cstr_as_str(it->raw[n].name)); - sp_expect_eq(t, (u32)entry.kind, (u32)it->raw[n].kind); - } - - sp_test_kv_clear(t, "entry"); - sp_expect_eq(t, cursor, buf.len); - return SP_OK; -} - -#endif diff --git a/test/sys/open.c b/test/sys/open.c index e6486db8..029a438f 100644 --- a/test/sys/open.c +++ b/test/sys/open.c @@ -257,6 +257,36 @@ static const sys_case_t sys_open_dir_cases [] = { { .kind = SYS_STEP_OPEN_DIR, .open_dir = { .path = "dir", .err = SP_ERR_SYS_NOT_FOUND } }, }, }, + { + .name = "follows_dir_symlink", + .setup = { + { .path = "dir", .kind = SYS_SETUP_DIR }, + { .path = "link", .kind = SYS_SETUP_DIR_SYMLINK, .target = "dir" }, + }, + .steps = { + { .kind = SYS_STEP_OPEN_DIR, .open_dir = { .path = "link" } }, + }, + }, + { + .name = "nofollow_refuses_dir_symlink", + .setup = { + { .path = "dir", .kind = SYS_SETUP_DIR }, + { .path = "link", .kind = SYS_SETUP_DIR_SYMLINK, .target = "dir" }, + }, + .steps = { + { .kind = SYS_STEP_OPEN_DIR, .open_dir = { .path = "link", .flags = SP_SYS_OPEN_DIR_NO_FOLLOW, .err = SP_ERR_SYS_LOOP } }, + }, + }, + { + .name = "nofollow_refuses_file_symlink", + .setup = { + { .path = "file.bin", .content = "A" }, + { .path = "link", .kind = SYS_SETUP_SYMLINK, .target = "file.bin" }, + }, + .steps = { + { .kind = SYS_STEP_OPEN_DIR, .open_dir = { .path = "link", .flags = SP_SYS_OPEN_DIR_NO_FOLLOW, .err = SP_ERR_SYS_LOOP } }, + }, + }, }; sp_test_each_fn(sys, open, sys_case_t, sys_open_cases, sys_case_run); diff --git a/test/sys/path.c b/test/sys/path.c index 326c7d0d..46218a48 100644 --- a/test/sys/path.c +++ b/test/sys/path.c @@ -32,9 +32,11 @@ typedef struct { const c8* dir; const c8* link; const c8* target; + bool dir_link; const c8* exists; const c8* not_exists; sp_fs_kind_t kind; + bool posix; } test_t; static const test_t tests [] = { @@ -86,6 +88,7 @@ static const test_t tests [] = { .dir = "dir", .link = "lnk", .target = "dir", + .dir_link = true, .exists = "dir", .not_exists = "lnk", }, @@ -115,6 +118,7 @@ static const test_t tests [] = { .dir = "dir", .link = "lnk", .target = "dir", + .dir_link = true, .exists = "lnk", }, { @@ -129,13 +133,28 @@ static const test_t tests [] = { }; static const test_t slash_tests [] = { + { + .name = "mkdir_backslash_is_a_name", + .op = OP_MKDIR, + .path = "A\\", + .exists = "A\\", + .not_exists = "A", + .posix = true, + }, + { + .name = "stat_drive_is_a_name", + .op = OP_STAT, + .path = "A:/", + .file = "A:", + .kind = SP_FS_KIND_FILE, + .posix = true, + }, { .name = "unlink_slash_file", .op = OP_UNLINK, .path = "file.bin/", - .err = SP_ERR_SYS_NOT_DIR, .file = "file.bin", - .exists = "file.bin", + .not_exists = "file.bin", }, { .name = "unlink_slash_dir", @@ -145,34 +164,24 @@ static const test_t slash_tests [] = { .dir = "dir", .exists = "dir", }, - { - .name = "unlink_slash_file_link", - .op = OP_UNLINK, - .path = "lnk/", - .err = SP_ERR_SYS_NOT_DIR, - .file = "file.bin", - .link = "lnk", - .target = "file.bin", - .exists = "file.bin", - }, { .name = "unlink_slash_dir_link", .op = OP_UNLINK, .path = "lnk/", - .err = SP_ERR_SYS_NOT_DIR, .dir = "dir", .link = "lnk", .target = "dir", - .exists = "lnk", + .dir_link = true, + .exists = "dir", + .not_exists = "lnk", }, { .name = "unlink_slash_dangling_link", .op = OP_UNLINK, .path = "lnk/", - .err = SP_ERR_SYS_NOT_DIR, .link = "lnk", .target = "missing", - .exists = "lnk", + .not_exists = "lnk", }, { .name = "rmdir_slash_dir", @@ -182,22 +191,19 @@ static const test_t slash_tests [] = { .not_exists = "dir", }, { - .name = "rmdir_slash_file", + .name = "rmdir_double_slash_dir", .op = OP_RMDIR, - .path = "file.bin/", - .err = SP_ERR_SYS_NOT_DIR, - .file = "file.bin", - .exists = "file.bin", + .path = "dir//", + .dir = "dir", + .not_exists = "dir", }, { - .name = "rmdir_slash_file_link", + .name = "rmdir_slash_file", .op = OP_RMDIR, - .path = "lnk/", + .path = "file.bin/", .err = SP_ERR_SYS_NOT_DIR, .file = "file.bin", - .link = "lnk", - .target = "file.bin", - .exists = "lnk", + .exists = "file.bin", }, { .name = "rmdir_slash_dir_link", @@ -207,6 +213,7 @@ static const test_t slash_tests [] = { .dir = "dir", .link = "lnk", .target = "dir", + .dir_link = true, .exists = "dir", }, { @@ -228,8 +235,8 @@ static const test_t slash_tests [] = { .name = "stat_slash_file", .op = OP_STAT, .path = "file.bin/", - .err = SP_ERR_SYS_NOT_DIR, .file = "file.bin", + .kind = SP_FS_KIND_FILE, }, { .name = "stat_slash_dir", @@ -242,19 +249,10 @@ static const test_t slash_tests [] = { .name = "stat_slash_file_link", .op = OP_STAT, .path = "lnk/", - .err = SP_ERR_SYS_NOT_DIR, .file = "file.bin", .link = "lnk", .target = "file.bin", - }, - { - .name = "stat_slash_dir_link", - .op = OP_STAT, - .path = "lnk/", - .dir = "dir", - .link = "lnk", - .target = "dir", - .kind = SP_FS_KIND_DIR, + .kind = SP_FS_KIND_FILE, }, { .name = "stat_slash_dangling_link", @@ -274,24 +272,8 @@ static const test_t slash_tests [] = { .name = "lstat_slash_file", .op = OP_LSTAT, .path = "file.bin/", - .err = SP_ERR_SYS_NOT_DIR, .file = "file.bin", - }, - { - .name = "lstat_slash_dir", - .op = OP_LSTAT, - .path = "dir/", - .dir = "dir", - .kind = SP_FS_KIND_DIR, - }, - { - .name = "lstat_slash_file_link", - .op = OP_LSTAT, - .path = "lnk/", - .err = SP_ERR_SYS_NOT_DIR, - .file = "file.bin", - .link = "lnk", - .target = "file.bin", + .kind = SP_FS_KIND_FILE, }, { .name = "lstat_slash_dir_link", @@ -300,24 +282,24 @@ static const test_t slash_tests [] = { .dir = "dir", .link = "lnk", .target = "dir", - .kind = SP_FS_KIND_DIR, + .dir_link = true, + .kind = SP_FS_KIND_SYMLINK, }, { .name = "lstat_slash_dangling_link", .op = OP_LSTAT, .path = "lnk/", - .err = SP_ERR_SYS_NOT_FOUND, .link = "lnk", .target = "missing", + .kind = SP_FS_KIND_SYMLINK, }, { .name = "unlink_slash_file_abs", .op = OP_UNLINK, .path_kind = PATH_ABS, .path = "file.bin/", - .err = SP_ERR_SYS_NOT_DIR, .file = "file.bin", - .exists = "file.bin", + .not_exists = "file.bin", }, { .name = "rmdir_slash_dir_abs", @@ -339,8 +321,8 @@ static const test_t slash_tests [] = { .op = OP_STAT, .path_kind = PATH_ABS, .path = "file.bin/", - .err = SP_ERR_SYS_NOT_DIR, .file = "file.bin", + .kind = SP_FS_KIND_FILE, }, { .name = "lstat_slash_dir_link_abs", @@ -350,7 +332,8 @@ static const test_t slash_tests [] = { .dir = "dir", .link = "lnk", .target = "dir", - .kind = SP_FS_KIND_DIR, + .dir_link = true, + .kind = SP_FS_KIND_SYMLINK, }, }; @@ -392,25 +375,29 @@ static bool check_err(sp_test_t* t, sp_err_t err, sp_err_t want) { } static sp_err_t run(sp_test_t* t, test_t* c) { + if (c->posix && sp_os_get_path_kind() != SP_FS_PATH_POSIX) { + return sp_test_skip(t, "posix paths only"); + } + + if (c->link) sp_test_skip_without_symlinks(); + if (c->path_kind == PATH_ABS) sp_test_skip_without_absolute_names(); + sp_mem_t mem = sp_test_arena(t); - sp_str_t sandbox = sp_test_dir(t); - sp_sys_fd_t sandbox_fd = SP_SYS_INVALID_FD; - sp_try(sp_sys_open_dir_s(sp_sys_get_root(0), sandbox, &sandbox_fd)); + sp_path_t sandbox = sp_test_dir(t); + if (c->file) sp_must_ok(t, sp_fs_create_file_str_at(sp_path_join(mem, sandbox, sp_cstr_as_str(c->file)), sp_str_lit("A"))); + if (c->dir) sp_must_ok(t, sp_fs_create_dir_at(sp_path_join(mem, sandbox, sp_cstr_as_str(c->dir)))); + if (c->link) sp_must_ok(t, sp_fs_create_sym_link_at(sp_cstr_as_str(c->target), sp_path_join(mem, sandbox, sp_cstr_as_str(c->link)), c->dir_link ? SP_FS_KIND_DIR : SP_FS_KIND_FILE)); - if (c->file) sp_fs_create_file_str(sp_fs_join_path(mem, sandbox, sp_cstr_as_str(c->file)), sp_str_lit("A")); - if (c->dir) sp_fs_create_dir(sp_fs_join_path(mem, sandbox, sp_cstr_as_str(c->dir))); - if (c->link) { - sp_str_t target = sp_fs_join_path(mem, sandbox, sp_cstr_as_str(c->target)); - sp_str_t link = sp_fs_join_path(mem, sandbox, sp_cstr_as_str(c->link)); - if (sp_fs_create_sym_link(target, link)) { - sp_sys_close(sandbox_fd); - return sp_test_skip(t, "symlinks not available"); - } - } + sp_sys_fd_t sandbox_fd = SP_SYS_INVALID_FD; + sp_try(sp_sys_open_dir_s(sandbox.dir, sandbox.sub, 0, &sandbox_fd)); sp_str_t path = sp_cstr_as_str(c->path); if (c->path_kind == PATH_LONG) path = long_path(mem, c->path); - if (c->path_kind == PATH_ABS) path = abs_path(mem, sandbox, c->path); + if (c->path_kind == PATH_ABS) { + sp_str_t root = sp_zero; + sp_must_ok(t, sp_fs_canonicalize_path_at(mem, sandbox, &root)); + path = abs_path(mem, root, c->path); + } sp_sys_file_meta_t meta = sp_zero; sp_err_t err = SP_OK; @@ -439,95 +426,3 @@ static sp_err_t run(sp_test_t* t, test_t* c) { sp_test_each_fn(sys, path, test_t, tests, run); sp_test_each_fn(sys, slash, test_t, slash_tests, run); - -sp_test(sys, canonicalize_refuses_overflow) { - sp_mem_t mem = sp_test_arena(t); - sp_str_t path = sp_fs_join_path(mem, sp_test_dir(t), sp_str_lit("file.bin")); - sp_fs_create_file(path); - - c8 full [SP_PATH_MAX] = sp_zero; - s64 len = sp_sys_canonicalize_path_s(path, full, sizeof(full)); - if (len <= 0) { - sp_test_fail(t, "canonicalize failed with a full-size buffer"); - return SP_OK; - } - - c8 buf [SP_PATH_MAX]; - sp_for(it, sizeof(buf)) buf[it] = (c8)0xAB; - s64 n = sp_sys_canonicalize_path_s(path, buf, (u64)len + 1); - if (n != len) { - sp_test_fail(t, "exact fit returned {} but expected {}", sp_fmt_int(n), sp_fmt_int(len)); - } - else if (buf[len] != 0) { - sp_test_fail(t, "exact fit did not NUL-terminate"); - } - else if (!sp_mem_is_equal(buf, full, (u64)len)) { - sp_test_fail(t, "exact fit returned different path"); - } - - sp_for(it, sizeof(buf)) buf[it] = (c8)0xAB; - n = sp_sys_canonicalize_path_s(path, buf, (u64)len); - if (n != -1) { - sp_test_fail(t, "no room for NUL: returned {} but expected -1", sp_fmt_int(n)); - } - - sp_for(it, sizeof(buf)) buf[it] = (c8)0xAB; - n = sp_sys_canonicalize_path_s(path, buf, (u64)len - 1); - if (n != -1) { - sp_test_fail(t, "undersized buffer: returned {} but expected -1", sp_fmt_int(n)); - } - - return SP_OK; -} - -sp_test(sys, canonicalize_ignores_read_permission) { - sp_mem_t mem = sp_test_arena(t); - sp_str_t path = sp_fs_join_path(mem, sp_test_dir(t), sp_str_lit("file.bin")); - sp_fs_create_file(path); - -#if defined(SP_POSIX) - if (geteuid() == 0) return sp_test_skip(t, "running as root"); -#endif - - sp_err_t err = sp_sys_set_file_perms_s(sp_sys_get_root(0), path, sp_zero_s(sp_sys_file_perms_t)); - if (err == SP_ERR_SYS_UNSUPPORTED) return sp_test_skip(t, "set_file_perms not supported"); - sp_try(err); - - c8 buf [SP_PATH_MAX] = sp_zero; - s64 n = sp_sys_canonicalize_path_s(path, buf, sizeof(buf)); - if (n <= 0) sp_test_fail(t, "canonicalize failed on an unreadable file"); - return SP_OK; -} - -sp_test(sys, canonicalize_does_not_block_on_fifo) { -#if defined(SP_POSIX) - sp_mem_t mem = sp_test_arena(t); - sp_str_t path = sp_fs_join_path(mem, sp_test_dir(t), sp_str_lit("fifo")); - if (mkfifo(sp_str_to_cstr(mem, path), 0644)) return sp_test_skip(t, "fifos not available"); - - pid_t pid = fork(); - if (pid < 0) return sp_test_skip(t, "fork not available"); - if (pid == 0) { - c8 buf [SP_PATH_MAX] = sp_zero; - _exit(sp_sys_canonicalize_path_s(path, buf, sizeof(buf)) > 0 ? 0 : 1); - } - - s32 status = 0; - sp_for(it, 100) { - if (waitpid(pid, &status, WNOHANG) == pid) { - if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { - sp_test_fail(t, "canonicalize failed on a fifo"); - } - return SP_OK; - } - sp_os_sleep_ms(5); - } - - sp_test_fail(t, "canonicalize blocked on a fifo with no writer"); - kill(pid, SIGKILL); - waitpid(pid, &status, 0); - return SP_OK; -#else - return sp_test_skip(t, "no fifos"); -#endif -} diff --git a/test/sys/perms.c b/test/sys/perms.c index 01af08fc..2bc3ecb3 100644 --- a/test/sys/perms.c +++ b/test/sys/perms.c @@ -35,12 +35,12 @@ static const test_t tests [] = { }; static sp_err_t run(sp_test_t* t, test_t* it) { - sp_str_t path = sp_fs_join_path(sp_test_arena(t), sp_test_dir(t), sp_str_lit("A")); - sp_fs_create_file(path); - sp_sys_fd_t root = sp_sys_get_root(0); + sp_test_skip_on_wasm(); + sp_path_t path = sp_path_join(sp_test_arena(t), sp_test_dir(t), sp_str_lit("A")); + sp_fs_create_file_at(path); sp_sys_file_meta_t meta = sp_zero; - sp_err_t err = sp_sys_get_path_metadata_s(root, path, &meta); + sp_err_t err = sp_sys_get_path_metadata_s(path.dir, path.sub, &meta); if (err == SP_ERR_SYS_UNSUPPORTED) return sp_test_skip(t, "metadata not supported"); sp_try(err); @@ -52,10 +52,10 @@ static sp_err_t run(sp_test_t* t, test_t* it) { case OP_EXECUTABLE: sp_sys_set_executable(&meta.perms, op.value); break; } } - sp_must_ok(t, sp_sys_set_file_perms_s(root, path, meta.perms)); + sp_must_ok(t, sp_sys_set_file_perms_s(path.dir, path.sub, meta.perms)); sp_sys_file_meta_t after = sp_zero; - sp_must_ok(t, sp_sys_get_path_metadata_s(root, path, &after)); + sp_must_ok(t, sp_sys_get_path_metadata_s(path.dir, path.sub, &after)); sp_expect_eq(t, sp_sys_is_read_only(after.perms), it->expect.read_only); #if !defined(SP_WIN32) sp_expect_eq(t, (after.perms.value & 0111) != 0, it->expect.executable); @@ -66,40 +66,39 @@ static sp_err_t run(sp_test_t* t, test_t* it) { sp_test_each_fn(sys, perms, test_t, tests, run); sp_test(sys, default_perms_then_read_only) { - sp_str_t path = sp_fs_join_path(sp_test_arena(t), sp_test_dir(t), sp_str_lit("A")); - sp_fs_create_file(path); - sp_sys_fd_t root = sp_sys_get_root(0); + sp_path_t path = sp_path_join(sp_test_arena(t), sp_test_dir(t), sp_str_lit("A")); + sp_fs_create_file_at(path); - sp_err_t err = sp_sys_set_file_perms_s(root, path, sp_sys_default_file_perms); + sp_err_t err = sp_sys_set_file_perms_s(path.dir, path.sub, sp_sys_default_file_perms); if (err == SP_ERR_SYS_UNSUPPORTED) return sp_test_skip(t, "set_file_perms not supported"); sp_try(err); sp_sys_file_meta_t meta = sp_zero; - sp_must_ok(t, sp_sys_get_path_metadata_s(root, path, &meta)); + sp_must_ok(t, sp_sys_get_path_metadata_s(path.dir, path.sub, &meta)); sp_expect(t, !sp_sys_is_read_only(meta.perms)); sp_sys_set_read_only(&meta.perms, true); - sp_must_ok(t, sp_sys_set_file_perms_s(root, path, meta.perms)); + sp_must_ok(t, sp_sys_set_file_perms_s(path.dir, path.sub, meta.perms)); sp_sys_file_meta_t after = sp_zero; - sp_must_ok(t, sp_sys_get_path_metadata_s(root, path, &after)); + sp_must_ok(t, sp_sys_get_path_metadata_s(path.dir, path.sub, &after)); sp_expect(t, sp_sys_is_read_only(after.perms)); return SP_OK; } sp_test(sys, mkdir_applies_perms) { sp_test_skip_on_win32(); - sp_str_t path = sp_fs_join_path(sp_test_arena(t), sp_test_dir(t), sp_str_lit("D")); - sp_sys_fd_t root = sp_sys_get_root(0); + sp_test_skip_on_wasm(); + sp_path_t path = sp_path_join(sp_test_arena(t), sp_test_dir(t), sp_str_lit("D")); sp_sys_file_perms_t perms = sp_sys_default_dir_perms; sp_sys_set_read_only(&perms, true); - sp_err_t err = sp_sys_mkdir_s(root, path, perms); + sp_err_t err = sp_sys_mkdir_s(path.dir, path.sub, perms); if (err == SP_ERR_SYS_UNSUPPORTED) return sp_test_skip(t, "mkdir not supported"); sp_try(err); sp_sys_file_meta_t meta = sp_zero; - sp_must_ok(t, sp_sys_get_path_metadata_s(root, path, &meta)); + sp_must_ok(t, sp_sys_get_path_metadata_s(path.dir, path.sub, &meta)); sp_expect(t, sp_sys_is_read_only(meta.perms)); return SP_OK; } diff --git a/test/sys/readlink.c b/test/sys/readlink.c index 5107c696..341ea918 100644 --- a/test/sys/readlink.c +++ b/test/sys/readlink.c @@ -33,8 +33,8 @@ static const test_t tests [] = { static sp_err_t run(sp_test_t* t, test_t* c) { sp_mem_t mem = sp_test_arena(t); - sp_str_t sandbox = sp_test_dir(t); - sp_str_t path = sp_fs_join_path(mem, sandbox, sp_str_lit("L")); + sp_path_t sandbox = sp_test_dir(t); + sp_path_t path = sp_path_join(mem, sandbox, sp_str_lit("L")); sp_str_t expected = c->expect.target ? sp_cstr_as_str(c->expect.target) : sp_zero_s(sp_str_t); switch (c->setup) { @@ -42,20 +42,21 @@ static sp_err_t run(sp_test_t* t, test_t* c) { break; } case SETUP_FILE: { - sp_must_ok(t, sp_fs_create_file(path)); + sp_must_ok(t, sp_fs_create_file_at(path)); break; } case SETUP_LINK: { - if (sp_fs_create_sym_link(sp_cstr_as_str(c->target), path)) { - return sp_test_skip(t, "symlinks not available"); - } + sp_test_skip_without_symlinks(); + sp_must_ok(t, sp_fs_create_sym_link_at(sp_cstr_as_str(c->target), path, SP_FS_KIND_FILE)); break; } case SETUP_LINK_ABSOLUTE: { - expected = sp_fs_join_path(mem, sandbox, sp_cstr_as_str(c->target)); - if (sp_fs_create_sym_link(expected, path)) { - return sp_test_skip(t, "symlinks not available"); - } + sp_test_skip_without_absolute_names(); + sp_test_skip_without_symlinks(); + sp_str_t root = sp_zero; + sp_must_ok(t, sp_fs_canonicalize_path_at(mem, sandbox, &root)); + expected = sp_fs_join_path(mem, root, sp_cstr_as_str(c->target)); + sp_must_ok(t, sp_fs_create_sym_link_at(expected, path, SP_FS_KIND_FILE)); break; } } @@ -68,7 +69,7 @@ static sp_err_t run(sp_test_t* t, test_t* c) { c8 buf [SP_PATH_MAX]; sp_str_t target = sp_zero; - sp_err_t err = sp_sys_readlink_s(sp_sys_get_root(0), path, buf, c->size ? c->size : sizeof(buf), &target); + sp_err_t err = sp_sys_readlink_s(path.dir, path.sub, buf, c->size ? c->size : sizeof(buf), &target); if (err == SP_ERR_SYS_UNSUPPORTED) return sp_test_skip(t, "readlink not supported"); sp_expect_err_eq(t, err, c->expect.err); diff --git a/test/sys/root.c b/test/sys/root.c index 7477ea90..ca354681 100644 --- a/test/sys/root.c +++ b/test/sys/root.c @@ -3,6 +3,55 @@ UTEST_EMPTY_FIXTURE(sys_root) UTEST_F(sys_root, out_of_range_returns_invalid) { - SKIP_ON_WASM() EXPECT_EQ(sp_sys_get_root(1024), SP_SYS_INVALID_FD); } + +UTEST_F(sys_root, out_of_range_has_no_label) { + c8 buf [SP_PATH_MAX] = sp_zero; + u64 len = 1; + EXPECT_EQ(sp_sys_get_root_label(1024, buf, sizeof(buf), &len), SP_ERR_SYS_BAD_FD); + EXPECT_EQ(len, 0); +} + +UTEST_F(sys_root, every_root_has_a_label) { + for (s32 it = 0; sp_sys_get_root(it) != SP_SYS_INVALID_FD; it++) { + c8 buf [SP_PATH_MAX]; + sp_for(at, sizeof(buf)) buf[at] = (c8)0xAB; + u64 len = 0; + EXPECT_EQ(sp_sys_get_root_label(it, buf, sizeof(buf), &len), SP_OK); + EXPECT_EQ(buf[len], 0); + EXPECT_TRUE(len <= 1 || buf[len - 1] != '/'); + } +} + +UTEST_F(sys_root, native_root_serves_everything) { + SKIP_ON_WASM() + + c8 buf [SP_PATH_MAX] = sp_zero; + u64 len = 1; + EXPECT_EQ(sp_sys_get_root_label(0, buf, sizeof(buf), &len), SP_OK); + EXPECT_EQ(len, 0); +} + +UTEST_F(sys_root, wasm_handles_have_no_path) { +#if !defined(SP_WASM) + UTEST_SKIP("wasm only"); +#else + c8 buf [SP_PATH_MAX] = sp_zero; + u64 len = 1; + EXPECT_EQ(sp_sys_get_fd_path(sp_sys_stdout, buf, sizeof(buf), &len), SP_ERR_SYS_UNSUPPORTED); + EXPECT_EQ(len, 0); +#endif +} + +UTEST_F(sys_root, label_needs_room_for_nul) { + if (sp_sys_get_root(0) == SP_SYS_INVALID_FD) UTEST_SKIP("no roots"); + + c8 buf [SP_PATH_MAX] = sp_zero; + u64 len = 0; + EXPECT_EQ(sp_sys_get_root_label(0, buf, sizeof(buf), &len), SP_OK); + + u64 n = 1; + EXPECT_EQ(sp_sys_get_root_label(0, buf, len, &n), SP_ERR_SYS_NAME_TOO_LONG); + EXPECT_EQ(n, 0); +} diff --git a/test/sys/self_path.c b/test/sys/self_path.c index 654542b1..36fe8b4a 100644 --- a/test/sys/self_path.c +++ b/test/sys/self_path.c @@ -20,10 +20,21 @@ static sp_err_t run(sp_test_t* t, test_t* c) { c8 buf [SP_PATH_MAX]; sp_for(it, sizeof(buf)) buf[it] = (c8)0xAB; - s64 n = c->fn == PATH_CWD ? - sp_sys_get_cwd_path(buf, sizeof(buf)) : - sp_sys_get_exe_path(buf, sizeof(buf)); - if (n <= 0) return sp_test_skip(t, "not available"); + u64 n = 0; + switch (c->fn) { + case PATH_CWD: { + sp_err_t err = sp_sys_get_fd_path(sp_fs_get_cwd(), buf, sizeof(buf), &n); + if (err == SP_ERR_SYS_UNSUPPORTED) return sp_test_skip(t, "not available"); + sp_must_ok(t, err); + break; + } + case PATH_EXE: { + sp_err_t err = sp_sys_get_exe_path(buf, sizeof(buf), &n); + if (err == SP_ERR_SYS_UNSUPPORTED) return sp_test_skip(t, "not available"); + sp_must_ok(t, err); + break; + } + } if (buf[n] != 0) { sp_test_fail(t, "not NUL-terminated"); diff --git a/test/sys/symlink.c b/test/sys/symlink.c index 3d5b2628..e43a390f 100644 --- a/test/sys/symlink.c +++ b/test/sys/symlink.c @@ -6,7 +6,7 @@ UTEST_F(sys_symlink, honors_dirfd) { run_sys_test(utest_result, (sys_test_t) { .label = "sys_symlink_honors_dirfd", .steps = { - { .kind = SYS_STEP_SYMLINK, .symlink = { .target = "target", .alias = "lnk" } }, + { .kind = SYS_STEP_SYMLINK, .symlink = { .target = "target", .alias = "lnk", .kind = SP_FS_KIND_FILE } }, { .kind = SYS_STEP_LSTAT, .lstat = { .path = "lnk" } }, }, }); @@ -19,9 +19,33 @@ UTEST_F(sys_symlink, resolves_target_relative_to_link) { { .path = "target.bin", .content = "hello" }, }, .steps = { - { .kind = SYS_STEP_SYMLINK, .symlink = { .target = "target.bin", .alias = "lnk" } }, + { .kind = SYS_STEP_SYMLINK, .symlink = { .target = "target.bin", .alias = "lnk", .kind = SP_FS_KIND_FILE } }, { .kind = SYS_STEP_OPEN, .open = { .path = "lnk" } }, { .kind = SYS_STEP_READ, .read = { .count = 8, .expect = "hello" } }, }, }); } + +UTEST_F(sys_symlink, dir_kind_resolves_to_dir) { + run_sys_test(utest_result, (sys_test_t) { + .label = "sys_symlink_dir_kind_resolves_to_dir", + .setup = { + { .path = "D", .kind = SYS_SETUP_DIR }, + }, + .steps = { + { .kind = SYS_STEP_SYMLINK, .symlink = { .target = "D", .alias = "L", .kind = SP_FS_KIND_DIR } }, + { .kind = SYS_STEP_STAT, .stat = { .path = "L", .kind = SP_FS_KIND_DIR } }, + }, + }); +} + +UTEST_F(sys_symlink, refuses_untyped_link) { + run_sys_test(utest_result, (sys_test_t) { + .label = "sys_symlink_refuses_untyped_link", + .steps = { + { .kind = SYS_STEP_SYMLINK, .symlink = { .target = "T", .alias = "L", .err = SP_ERR_SYS_INVALID } }, + { .kind = SYS_STEP_SYMLINK, .symlink = { .target = "T", .alias = "L", .kind = SP_FS_KIND_SYMLINK, .err = SP_ERR_SYS_INVALID } }, + { .kind = SYS_STEP_LSTAT, .lstat = { .path = "L", .err = SP_ERR_SYS_NOT_FOUND } }, + }, + }); +} diff --git a/test/sys/sys.c b/test/sys/sys.c index c4e8524e..a26b2a6f 100644 --- a/test/sys/sys.c +++ b/test/sys/sys.c @@ -1,25 +1,9 @@ #include "harness.h" -static sp_err_t sys_probe_symlinks(void* user) { - sp_str_t dir = *(sp_str_t*)user; - - sp_mem_arena_marker_t scratch = sp_mem_begin_scratch(); - sp_str_t target = sp_fs_join_path(scratch.mem, dir, sp_str_lit("probe_target")); - sp_str_t link = sp_fs_join_path(scratch.mem, dir, sp_str_lit("probe_link")); - - sp_fs_create_file(target); - sp_err_t err = sp_fs_create_sym_link(target, link); - if (!err) sp_fs_remove_file(link); - sp_fs_remove_file(target); - - sp_mem_end_scratch(scratch); - return err; -} - static bool sys_case_wants_symlinks(sys_case_t* c) { sp_carr_for(c->setup, it) { if (!c->setup[it].path) break; - if (c->setup[it].kind == SYS_SETUP_SYMLINK) return true; + if (c->setup[it].kind == SYS_SETUP_SYMLINK || c->setup[it].kind == SYS_SETUP_DIR_SYMLINK) return true; } return false; } @@ -49,40 +33,40 @@ sp_err_t sys_case_run(sp_test_t* t, sys_case_t* c) { sp_carr_for(fds, it) fds[it] = SP_SYS_INVALID_FD; sp_sys_fd_t sandbox_fd = SP_SYS_INVALID_FD; - sp_mem_t mem = sp_test_arena(t); - sp_str_t sandbox = sp_test_dir(t); - - if (sys_case_wants_symlinks(c) && sp_test_once(&sys_symlink_probe, sys_probe_symlinks, &sandbox)) { - return sp_test_skip(t, "symlinks not available"); - } + if (sys_case_wants_symlinks(c)) sp_test_skip_without_symlinks(); - sp_try(sp_sys_open_dir_s(sp_sys_get_root(0), sandbox, &sandbox_fd)); + sp_mem_t mem = sp_test_arena(t); + sp_path_t sandbox = sp_test_dir(t); sp_carr_for(c->setup, it) { sys_setup_t* ent = &c->setup[it]; if (!ent->path) break; - sp_str_t path = sp_fs_join_path(mem, sandbox, sp_cstr_as_str(ent->path)); + sp_path_t path = sp_path_join(mem, sandbox, sp_cstr_as_str(ent->path)); switch (ent->kind) { case SYS_SETUP_FILE: { - sp_fs_create_file_str(path, ent->content ? sp_cstr_as_str(ent->content) : sp_str_lit("")); + sp_fs_create_file_str_at(path, ent->content ? sp_cstr_as_str(ent->content) : sp_str_lit("")); break; } case SYS_SETUP_DIR: { - sp_fs_create_dir(path); + sp_fs_create_dir_at(path); break; } - case SYS_SETUP_SYMLINK: { - sp_str_t target = sp_fs_join_path(mem, sandbox, sp_cstr_as_str(ent->target)); - if (sp_fs_create_sym_link(target, path)) { - sp_test_fail(t, "failed to create symlink {} -> {}", sp_fmt_str(path), sp_fmt_str(target)); - goto done; + case SYS_SETUP_SYMLINK: + case SYS_SETUP_DIR_SYMLINK: { + sp_str_t target = sp_cstr_as_str(ent->target); + sp_fs_kind_t kind = ent->kind == SYS_SETUP_DIR_SYMLINK ? SP_FS_KIND_DIR : SP_FS_KIND_FILE; + if (sp_fs_create_sym_link_at(target, path, kind)) { + sp_test_fail(t, "failed to create symlink {} -> {}", sp_fmt_cstr(ent->path), sp_fmt_str(target)); + return SP_OK; } break; } } } + sp_try(sp_sys_open_dir_s(sandbox.dir, sandbox.sub, 0, &sandbox_fd)); + sp_carr_for(c->steps, it) { sys_step_t* step = &c->steps[it]; if (step->kind == SYS_STEP_NONE) break; @@ -104,7 +88,7 @@ sp_err_t sys_case_run(sp_test_t* t, sys_case_t* c) { case SYS_STEP_OPEN_DIR: { sp_sys_fd_t fd = SP_SYS_INVALID_FD; sp_str_t path = sp_cstr_as_str(step->open_dir.path); - sp_err_t err = sp_sys_open_dir_s(sandbox_fd, path, &fd); + sp_err_t err = sp_sys_open_dir_s(sandbox_fd, path, step->open_dir.flags, &fd); sys_check_err(t, err, step->open_dir.err); if (!err) { if (step->open_dir.err) sp_sys_close(fd); @@ -165,20 +149,20 @@ sp_err_t sys_case_run(sp_test_t* t, sys_case_t* c) { sp_carr_for(c->expect, it) { sys_expect_t* ent = &c->expect[it]; if (!ent->path) break; - sp_str_t path = sp_fs_join_path(mem, sandbox, sp_cstr_as_str(ent->path)); + sp_path_t path = sp_path_join(mem, sandbox, sp_cstr_as_str(ent->path)); - bool exists = sp_fs_exists(path); + bool exists = sp_fs_exists_at(path); if (exists != ent->exists) { - sp_test_fail(t, "expected {} {} exist", sp_fmt_str(path), sp_fmt_cstr(ent->exists ? "to" : "not to")); + sp_test_fail(t, "expected {} {} exist", sp_fmt_cstr(ent->path), sp_fmt_cstr(ent->exists ? "to" : "not to")); continue; } if (ent->content) { sp_str_t actual = sp_zero; - sp_io_read_file(mem, path, &actual); + sp_io_read_file_at(mem, path, &actual); if (!sp_str_equal(actual, sp_cstr_as_str(ent->content))) { sp_test_record(t, (sp_test_failure_t) { - .message = sp_test_format(t, "content of {}", sp_fmt_str(path)), + .message = sp_test_format(t, "content of {}", sp_fmt_cstr(ent->path)), .expected = sp_test_format(t, "{.quote}", sp_fmt_cstr(ent->content)), .actual = sp_test_format(t, "{.quote}", sp_fmt_str(actual)), }); @@ -186,7 +170,6 @@ sp_err_t sys_case_run(sp_test_t* t, sys_case_t* c) { } } -done: - if (sandbox_fd != SP_SYS_INVALID_FD) sp_sys_close(sandbox_fd); + sp_sys_close(sandbox_fd); return SP_OK; } diff --git a/test/sys/sys.h b/test/sys/sys.h index ea9afac9..bc520b5f 100644 --- a/test/sys/sys.h +++ b/test/sys/sys.h @@ -21,7 +21,7 @@ static void sys_probe_symlinks(sp_test_file_manager_t* fm) { sp_str_t target = sp_test_file_path_c(fm, ".sys_symlink_probe_target"); sp_str_t link = sp_test_file_path_c(fm, ".sys_symlink_probe_link"); sp_fs_create_file(target); - sys_symlinks_available = sp_fs_create_sym_link(target, link) == SP_OK; + sys_symlinks_available = sp_fs_create_sym_link(target, link, SP_FS_KIND_FILE) == SP_OK; if (sys_symlinks_available) sp_fs_remove_file(link); sp_fs_remove_file(target); } @@ -30,6 +30,7 @@ typedef enum { SYS_SETUP_FILE, SYS_SETUP_DIR, SYS_SETUP_SYMLINK, + SYS_SETUP_DIR_SYMLINK, SYS_SETUP_READONLY, } sys_setup_kind_t; @@ -80,7 +81,7 @@ typedef struct { struct { u32 slot; u64 count; u64 offset; const c8* expect; } pread; struct { u32 slot; const c8* data; sp_err_t err; } write; struct { u32 slot; const c8* data; u64 offset; } pwrite; - struct { const c8* target; const c8* alias; } symlink; + struct { const c8* target; const c8* alias; sp_fs_kind_t kind; sp_err_t err; } symlink; struct { const c8* from; sys_dir_t from_dir; const c8* to; sys_dir_t to_dir; sp_err_t err; } rename; struct { const c8* path; sp_fs_kind_t kind; sp_err_t err; } lstat; struct { const c8* path; sp_fs_kind_t kind; s64 size; sp_err_t err; } stat; @@ -108,7 +109,7 @@ typedef struct { static bool sys_test_wants_symlinks(sys_test_t* t) { sp_carr_for(t->setup, it) { if (!t->setup[it].path) break; - if (t->setup[it].kind == SYS_SETUP_SYMLINK) return true; + if (t->setup[it].kind == SYS_SETUP_SYMLINK || t->setup[it].kind == SYS_SETUP_DIR_SYMLINK) return true; } sp_carr_for(t->steps, it) { if (t->steps[it].kind == SYS_STEP_NONE) break; @@ -173,9 +174,11 @@ static bool sys_apply_setup(s32* utest_result, sp_test_file_manager_t* fm, sp_st sp_fs_create_dir(path); break; } - case SYS_SETUP_SYMLINK: { - sp_str_t target = sp_fs_join_path(fm->mem, sandbox, sp_cstr_as_str(ent->target)); - if (sp_fs_create_sym_link(target, path) != SP_OK) { + case SYS_SETUP_SYMLINK: + case SYS_SETUP_DIR_SYMLINK: { + sp_str_t target = sp_cstr_as_str(ent->target); + sp_fs_kind_t kind = ent->kind == SYS_SETUP_DIR_SYMLINK ? SP_FS_KIND_DIR : SP_FS_KIND_FILE; + if (sp_fs_create_sym_link(target, path, kind) != SP_OK) { SP_TEST_REPORT("failed to create symlink {} -> {}", sp_fmt_str(path), sp_fmt_str(target)); SP_FAIL(); return false; @@ -244,7 +247,7 @@ static void run_sys_test(s32* utest_result, sys_test_t t) { } sp_str_t sandbox = sp_test_file_create_dir(&fm, t.label); - if (sp_sys_open_dir_s(sp_sys_get_root(0), sandbox, &sandbox_fd)) { + if (sp_sys_open_dir_s(sp_sys_get_root(0), sandbox, 0, &sandbox_fd)) { SP_TEST_REPORT("failed to open sandbox {}", sp_fmt_str(sandbox)); SP_FAIL(); goto done; @@ -275,7 +278,7 @@ static void run_sys_test(s32* utest_result, sys_test_t t) { } case SYS_STEP_OPEN_DIR: { sp_sys_fd_t fd = SP_SYS_INVALID_FD; - if (sp_sys_open_dir_s(sandbox_fd, sp_cstr_as_str(step->open_dir.path), &fd)) { + if (sp_sys_open_dir_s(sandbox_fd, sp_cstr_as_str(step->open_dir.path), 0, &fd)) { SP_TEST_REPORT("failed to open_dir {}", sp_fmt_cstr(step->open_dir.path)); SP_FAIL(); } @@ -342,8 +345,8 @@ static void run_sys_test(s32* utest_result, sys_test_t t) { break; } case SYS_STEP_SYMLINK: { - sp_err_t err = sp_sys_symlink_s(sp_cstr_as_str(step->symlink.target), sandbox_fd, sp_cstr_as_str(step->symlink.alias)); - sys_expect_err(utest_result, "symlink", err, SP_OK); + sp_err_t err = sp_sys_symlink_s(sp_cstr_as_str(step->symlink.target), sandbox_fd, sp_cstr_as_str(step->symlink.alias), step->symlink.kind); + sys_expect_err(utest_result, "symlink", err, step->symlink.err); break; } case SYS_STEP_RENAME: { diff --git a/test/sys/timestamps.c b/test/sys/timestamps.c index a2af087d..955a03d8 100644 --- a/test/sys/timestamps.c +++ b/test/sys/timestamps.c @@ -31,13 +31,11 @@ static s64 field_ns(const sp_sys_file_meta_t* meta, field_t field) { } static sp_err_t run(sp_test_t* t, test_t* c) { - sp_mem_t mem = sp_test_arena(t); - sp_str_t path = sp_fs_join_path(mem, sp_test_dir(t), sp_str_lit("A")); - sp_fs_create_file_str(path, sp_str_lit("A")); + sp_path_t path = sp_path_join(sp_test_arena(t), sp_test_dir(t), sp_str_lit("A")); + sp_fs_create_file_str_at(path, sp_str_lit("A")); - sp_sys_fd_t root = sp_sys_get_root(0); sp_sys_file_meta_t before = sp_zero; - sp_err_t err = sp_sys_get_path_metadata_s(root, path, &before); + sp_err_t err = sp_sys_get_path_metadata_s(path.dir, path.sub, &before); if (err == SP_ERR_SYS_UNSUPPORTED) return sp_test_skip(t, "metadata not supported"); sp_try(err); @@ -46,22 +44,23 @@ static sp_err_t run(sp_test_t* t, test_t* c) { switch (c->op) { case OP_WRITE: { sp_sys_fd_t fd = SP_SYS_INVALID_FD; - sp_must_ok(t, sp_sys_open_s(root, path, SP_SYS_OPEN_MODE_WO, SP_SYS_OPEN_APPEND, &fd)); + sp_must_ok(t, sp_sys_open_s(path.dir, path.sub, SP_SYS_OPEN_MODE_WO, SP_SYS_OPEN_APPEND, &fd)); u64 n = 0; sp_expect_ok(t, sp_sys_write(fd, "B", 1, &n)); sp_sys_close(fd); break; } case OP_SET_PERMS: { + sp_test_skip_on_wasm(); sp_sys_file_perms_t perms = before.perms; sp_sys_set_read_only(&perms, true); - sp_must_ok(t, sp_sys_set_file_perms_s(root, path, perms)); + sp_must_ok(t, sp_sys_set_file_perms_s(path.dir, path.sub, perms)); break; } } sp_sys_file_meta_t after = sp_zero; - sp_try(sp_sys_get_path_metadata_s(root, path, &after)); + sp_try(sp_sys_get_path_metadata_s(path.dir, path.sub, &after)); bool moved = field_ns(&before, c->field) != field_ns(&after, c->field); if (moved != c->moves) { diff --git a/test/sys/transfer.c b/test/sys/transfer.c index 73e34125..776ce9f8 100644 --- a/test/sys/transfer.c +++ b/test/sys/transfer.c @@ -114,20 +114,19 @@ static sp_err_t run(sp_test_t* t, test_t* c) { #endif sp_mem_t mem = sp_test_arena(t); - sp_str_t sandbox = sp_test_dir(t); - sp_sys_fd_t sandbox_fd = SP_SYS_INVALID_FD; + sp_path_t sandbox = sp_test_dir(t); + sp_path_t src = sp_path_join(mem, sandbox, sp_str_lit("src")); + sp_path_t dst = sp_path_join(mem, sandbox, sp_str_lit("dst")); sp_sys_fd_t in = SP_SYS_INVALID_FD; sp_sys_fd_t out = SP_SYS_INVALID_FD; sp_sys_fd_t src_w = SP_SYS_INVALID_FD; sp_sys_fd_t dst_r = SP_SYS_INVALID_FD; u64 len = sp_cstr_len(c->data); - sp_try(sp_sys_open_dir_s(sp_sys_get_root(0), sandbox, &sandbox_fd)); - switch (c->src) { case END_FILE: { - sp_fs_create_file_str(sp_fs_join_path(mem, sandbox, sp_str_lit("src")), sp_cstr_as_str(c->data)); - sp_must_ok(t, sp_sys_open_s(sandbox_fd, sp_str_lit("src"), SP_SYS_OPEN_MODE_RO, 0, &in)); + sp_must_ok(t, sp_fs_create_file_str_at(src, sp_cstr_as_str(c->data))); + sp_must_ok(t, sp_sys_open_s(src.dir, src.sub, SP_SYS_OPEN_MODE_RO, 0, &in)); break; } case END_PIPE: { @@ -146,10 +145,10 @@ static sp_err_t run(sp_test_t* t, test_t* c) { case END_FILE: { u32 flags = SP_SYS_OPEN_CREATE; if (c->prefill) { - sp_fs_create_file_str(sp_fs_join_path(mem, sandbox, sp_str_lit("dst")), sp_cstr_as_str(c->prefill)); + sp_must_ok(t, sp_fs_create_file_str_at(dst, sp_cstr_as_str(c->prefill))); flags = 0; } - sp_must_ok(t, sp_sys_open_s(sandbox_fd, sp_str_lit("dst"), SP_SYS_OPEN_MODE_WO, flags, &out)); + sp_must_ok(t, sp_sys_open_s(dst.dir, dst.sub, SP_SYS_OPEN_MODE_WO, flags, &out)); break; } case END_PIPE: { @@ -183,7 +182,7 @@ static sp_err_t run(sp_test_t* t, test_t* c) { sp_sys_close(out); out = SP_SYS_INVALID_FD; sp_str_t actual = sp_zero; - sp_io_read_file(mem, sp_fs_join_path(mem, sandbox, sp_str_lit("dst")), &actual); + sp_io_read_file_at(mem, dst, &actual); sp_expect_str_eq_c(t, actual, c->expect); break; } @@ -201,7 +200,6 @@ static sp_err_t run(sp_test_t* t, test_t* c) { if (out != SP_SYS_INVALID_FD) sp_sys_close(out); if (src_w != SP_SYS_INVALID_FD) sp_sys_close(src_w); if (dst_r != SP_SYS_INVALID_FD) sp_sys_close(dst_r); - if (sandbox_fd != SP_SYS_INVALID_FD) sp_sys_close(sandbox_fd); return SP_OK; } diff --git a/test/sys/vtable.c b/test/sys/vtable.c deleted file mode 100644 index 4faf9c41..00000000 --- a/test/sys/vtable.c +++ /dev/null @@ -1,771 +0,0 @@ -#include "sp.h" -#include "sp/sp_test.h" - -typedef struct { - const c8* name; - s64 (*call)(void); - s64 expect; -} test_t; - -static s64 recorded; - -static sp_err_t mock_read(sp_sys_fd_t fd, void* buf, u64 count, u64* bytes_read) { - return (sp_err_t)69; -} - -static sp_err_t mock_write(sp_sys_fd_t fd, const void* buf, u64 count, u64* bytes_written) { - return (sp_err_t)69; -} - -static sp_err_t mock_pread(sp_sys_fd_t fd, void* buf, u64 count, u64 offset, u64* bytes_read) { - return (sp_err_t)69; -} - -static sp_err_t mock_pwrite(sp_sys_fd_t fd, const void* buf, u64 count, u64 offset, u64* bytes_written) { - return (sp_err_t)69; -} - -static sp_err_t mock_transfer(sp_sys_fd_t in, u64* in_pos, sp_sys_fd_t out, u64 count, u64* bytes_moved) { - return (sp_err_t)69; -} - -static sp_err_t mock_transfer_positional(sp_sys_fd_t in, u64* in_pos, sp_sys_fd_t out, u64 count, u64 offset, u64* bytes_moved) { - return (sp_err_t)69; -} - -static sp_sys_fd_t mock_get_root(s32 it) { - return 69; -} - -static s64 mock_get_exe_path(c8* buf, u64 size) { - return 69; -} - -static s64 mock_get_cwd_path(c8* buf, u64 size) { - return 69; -} - -static s64 mock_get_storage_path(c8* buf, u64 size) { - return 69; -} - -static s64 mock_get_config_path(c8* buf, u64 size) { - return 69; -} - -static sp_err_t mock_open(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_open_mode_t mode, u32 flags, sp_sys_fd_t* out) { - *out = 69; - return (sp_err_t)69; -} - -static sp_err_t mock_open_dir(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_fd_t* out) { - *out = 69; - return (sp_err_t)69; -} - -static sp_err_t mock_close(sp_sys_fd_t fd) { - return (sp_err_t)69; -} - -static sp_err_t mock_pipe(sp_sys_pipe_t* pipe, sp_sys_pipe_desc_t desc) { - return (sp_err_t)69; -} - -static sp_err_t mock_pipe_ready(sp_sys_fd_t fd, u8* ready) { - return (sp_err_t)69; -} - -static sp_err_t mock_mkdir(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_perms_t perms) { - return (sp_err_t)69; -} - -static sp_err_t mock_rmdir(sp_sys_fd_t fd, const c8* path, u32 len) { - return (sp_err_t)69; -} - -static sp_err_t mock_unlink(sp_sys_fd_t fd, const c8* path, u32 len) { - return (sp_err_t)69; -} - -static sp_err_t mock_rename(sp_sys_fd_t from_fd, const c8* from, u32 from_len, sp_sys_fd_t to_fd, const c8* to, u32 to_len) { - return (sp_err_t)69; -} - -static sp_err_t mock_link(sp_sys_fd_t from_fd, const c8* existing, u32 existing_len, sp_sys_fd_t to_fd, const c8* alias, u32 alias_len) { - return (sp_err_t)69; -} - -static sp_err_t mock_symlink(const c8* existing, u32 existing_len, sp_sys_fd_t to_fd, const c8* alias, u32 alias_len) { - return (sp_err_t)69; -} - -static sp_err_t mock_readlink(sp_sys_fd_t fd, const c8* path, u32 len, c8* buf, u64 size, u64* target_len) { - return (sp_err_t)69; -} - -static sp_err_t mock_get_path_metadata(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_meta_t* st) { - return (sp_err_t)69; -} - -static sp_err_t mock_get_link_metadata(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_meta_t* st) { - return (sp_err_t)69; -} - -static sp_err_t mock_get_file_metadata(sp_sys_fd_t fd, sp_sys_file_meta_t* st) { - return (sp_err_t)69; -} - -static sp_err_t mock_set_file_perms(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_perms_t perms) { - return (sp_err_t)69; -} - -static sp_err_t mock_clock_gettime(s32 clockid, sp_sys_timespec_t* ts) { - return (sp_err_t)69; -} - -static sp_err_t mock_nanosleep(const sp_sys_timespec_t* req, sp_sys_timespec_t* rem) { - return (sp_err_t)69; -} - -static bool mock_futex_wait(u32* addr, u32 expected, const sp_sys_timespec_t* timeout) { - return true; -} - -static void mock_futex_wake(u32* addr) { - recorded = 69; -} - -static void mock_futex_wake_all(u32* addr) { - recorded = 69; -} - -static s64 mock_canonicalize_path(const c8* path, u32 len, c8* buf, u64 size) { - return 69; -} - -static sp_err_t mock_event_open(sp_sys_event_t* out) { - return (sp_err_t)69; -} - -static sp_err_t mock_event_signal(sp_sys_event_t event) { - return (sp_err_t)69; -} - -static sp_err_t mock_event_clear(sp_sys_event_t event) { - return (sp_err_t)69; -} - -static sp_err_t mock_wait(const sp_sys_fd_t* fds, u64 n, u32 timeout_ms, u64* signaled) { - return (sp_err_t)69; -} - -static sp_err_t mock_tty_get(sp_sys_fd_t fd, sp_sys_tty_attr_t* attr) { - return (sp_err_t)69; -} - -static sp_err_t mock_tty_set(sp_sys_fd_t fd, const sp_sys_tty_attr_t* attr) { - return (sp_err_t)69; -} - -static sp_err_t mock_tty_size(sp_sys_fd_t fd, u32* cols, u32* rows) { - return (sp_err_t)69; -} - -static bool mock_is_tty(sp_sys_fd_t fd) { - return true; -} - -static sp_err_t mock_tty_ready(sp_sys_fd_t fd, u8* ready) { - return (sp_err_t)69; -} - -static sp_err_t mock_tty_mode_apply(sp_sys_tty_attr_t* in, sp_sys_tty_attr_t* out, sp_sys_tty_mode_t mode) { - return (sp_err_t)69; -} - -static sp_err_t mock_tty_use_vt(sp_sys_fd_t fd) { - return (sp_err_t)69; -} - -static sp_err_t mock_socket_open(sp_sys_socket_t* out, sp_sys_handle_desc_t desc) { - return (sp_err_t)69; -} - -static sp_err_t mock_socket_bind(sp_sys_socket_t socket, sp_sys_ipv4_t addr) { - return (sp_err_t)69; -} - -static sp_err_t mock_socket_listen(sp_sys_socket_t socket, u32 backlog) { - return (sp_err_t)69; -} - -static sp_err_t mock_socket_connect(sp_sys_socket_t socket, sp_sys_ipv4_t addr) { - return (sp_err_t)69; -} - -static sp_err_t mock_socket_error(sp_sys_socket_t socket) { - return (sp_err_t)69; -} - -static sp_err_t mock_socket_accept(sp_sys_socket_t listener, sp_sys_handle_desc_t desc, sp_sys_socket_t* out) { - return (sp_err_t)69; -} - -static sp_err_t mock_socket_close(sp_sys_socket_t socket) { - return (sp_err_t)69; -} - -static sp_err_t mock_socket_recv(sp_sys_socket_t socket, void* buf, u64 count, u64* bytes_read) { - return (sp_err_t)69; -} - -static sp_err_t mock_socket_send(sp_sys_socket_t socket, const void* buf, u64 count, u64* bytes_written) { - return (sp_err_t)69; -} - -static sp_err_t mock_socket_wait(sp_sys_socket_t socket, bool readable, u32 timeout_ms) { - return (sp_err_t)69; -} - -static sp_err_t mock_socket_set_nonblocking(sp_sys_socket_t socket) { - return (sp_err_t)69; -} - -static sp_err_t mock_socket_reuse_addr(sp_sys_socket_t socket) { - return (sp_err_t)69; -} - -static sp_err_t mock_socket_local_port(sp_sys_socket_t socket, u16* out) { - return (sp_err_t)69; -} - -static void* mock_alloc(u64 size) { - return (void*)(uintptr_t)69; -} - -static void mock_free(void* ptr, u64 size) { - recorded = 69; -} - -static void* mock_memcpy(void* dest, const void* src, u64 n) { - return (void*)(uintptr_t)69; -} - -static void* mock_memmove(void* dest, const void* src, u64 n) { - return (void*)(uintptr_t)69; -} - -static void* mock_memset(void* dest, u8 fill, u64 n) { - return (void*)(uintptr_t)69; -} - -static s32 mock_memcmp(const void* a, const void* b, u64 n) { - return 69; -} - -static void mock_assert(bool cond) { - recorded = 69; -} - -static void mock_exit(s32 code) { - recorded = 69; -} - -static void mock_env(const c8** env, u32* len) { - recorded = 69; -} - -static s64 mock_lseek(sp_sys_fd_t fd, s64 offset, s32 whence) { - return 69; -} - -static sp_err_t mock_chdir(const c8* path, u32 len) { - return (sp_err_t)69; -} - -static sp_err_t mock_dir_from_fd(sp_sys_fd_t fd, sp_sys_dir_t* out) { - return (sp_err_t)69; -} - -static sp_err_t mock_dir_read(sp_sys_dir_t* dir, sp_mem_buffer_t* buf) { - return (sp_err_t)69; -} - -static sp_err_t mock_dir_parse(sp_sys_dir_t* dir, sp_mem_buffer_t* buf, u64* cursor, sp_sys_dir_entry_t* out) { - return (sp_err_t)69; -} - -static sp_err_t mock_dir_close(sp_sys_dir_t* dir) { - return (sp_err_t)69; -} - -static const sp_sys_vtable_t mock = { - .read = mock_read, - .write = mock_write, - .pread = mock_pread, - .pwrite = mock_pwrite, - .transfer = mock_transfer, - .transfer_positional = mock_transfer_positional, - .get_root = mock_get_root, - .get_exe_path = mock_get_exe_path, - .get_cwd_path = mock_get_cwd_path, - .get_storage_path = mock_get_storage_path, - .get_config_path = mock_get_config_path, - .open = mock_open, - .open_dir = mock_open_dir, - .close = mock_close, - .pipe = mock_pipe, - .pipe_ready = mock_pipe_ready, - .mkdir = mock_mkdir, - .rmdir = mock_rmdir, - .unlink = mock_unlink, - .rename = mock_rename, - .link = mock_link, - .symlink = mock_symlink, - .readlink = mock_readlink, - .get_path_metadata = mock_get_path_metadata, - .get_link_metadata = mock_get_link_metadata, - .get_file_metadata = mock_get_file_metadata, - .set_file_perms = mock_set_file_perms, - .clock_gettime = mock_clock_gettime, - .nanosleep = mock_nanosleep, - .futex_wait = mock_futex_wait, - .futex_wake = mock_futex_wake, - .futex_wake_all = mock_futex_wake_all, - .canonicalize_path = mock_canonicalize_path, - .event_open = mock_event_open, - .event_signal = mock_event_signal, - .event_clear = mock_event_clear, - .wait = mock_wait, - .tty_get = mock_tty_get, - .tty_set = mock_tty_set, - .tty_size = mock_tty_size, - .is_tty = mock_is_tty, - .tty_ready = mock_tty_ready, - .tty_mode_apply = mock_tty_mode_apply, - .tty_use_vt = mock_tty_use_vt, - .socket_open = mock_socket_open, - .socket_bind = mock_socket_bind, - .socket_listen = mock_socket_listen, - .socket_connect = mock_socket_connect, - .socket_error = mock_socket_error, - .socket_accept = mock_socket_accept, - .socket_close = mock_socket_close, - .socket_recv = mock_socket_recv, - .socket_send = mock_socket_send, - .socket_wait = mock_socket_wait, - .socket_set_nonblocking = mock_socket_set_nonblocking, - .socket_reuse_addr = mock_socket_reuse_addr, - .socket_local_port = mock_socket_local_port, - .alloc = mock_alloc, - .free = mock_free, - .memcpy = mock_memcpy, - .memmove = mock_memmove, - .memset = mock_memset, - .memcmp = mock_memcmp, - .assert = mock_assert, - .exit = mock_exit, - .env = mock_env, - .lseek = mock_lseek, - .chdir = mock_chdir, - .dir_from_fd = mock_dir_from_fd, - .dir_read = mock_dir_read, - .dir_parse = mock_dir_parse, - .dir_close = mock_dir_close, -}; - -static s64 call_read(void) { - return (s64)sp_sys_read(0, SP_NULLPTR, 0, SP_NULLPTR); -} - -static s64 call_write(void) { - return (s64)sp_sys_write(0, SP_NULLPTR, 0, SP_NULLPTR); -} - -static s64 call_pread(void) { - return (s64)sp_sys_pread(0, SP_NULLPTR, 0, 0, SP_NULLPTR); -} - -static s64 call_pwrite(void) { - return (s64)sp_sys_pwrite(0, SP_NULLPTR, 0, 0, SP_NULLPTR); -} - -static s64 call_transfer(void) { - return (s64)sp_sys_transfer(0, SP_NULLPTR, 0, 0, SP_NULLPTR); -} - -static s64 call_transfer_positional(void) { - return (s64)sp_sys_transfer_positional(0, SP_NULLPTR, 0, 0, 0, SP_NULLPTR); -} - -static s64 call_get_root(void) { - return (s64)sp_sys_get_root(0); -} - -static s64 call_get_exe_path(void) { - return sp_sys_get_exe_path(SP_NULLPTR, 0); -} - -static s64 call_get_cwd_path(void) { - return sp_sys_get_cwd_path(SP_NULLPTR, 0); -} - -static s64 call_get_storage_path(void) { - return sp_sys_get_storage_path(SP_NULLPTR, 0); -} - -static s64 call_get_config_path(void) { - return sp_sys_get_config_path(SP_NULLPTR, 0); -} - -static s64 call_open(void) { - sp_sys_fd_t out = SP_SYS_INVALID_FD; - sp_sys_open(0, SP_NULLPTR, 0, SP_SYS_OPEN_MODE_RO, 0, &out); - return (s64)out; -} - -static s64 call_open_dir(void) { - sp_sys_fd_t out = SP_SYS_INVALID_FD; - sp_sys_open_dir(0, SP_NULLPTR, 0, &out); - return (s64)out; -} - -static s64 call_close(void) { - return (s64)sp_sys_close(0); -} - -static s64 call_pipe(void) { - return (s64)sp_sys_pipe(SP_NULLPTR, sp_zero_s(sp_sys_pipe_desc_t)); -} - -static s64 call_pipe_ready(void) { - u8 ready = 0; - return (s64)sp_sys_pipe_ready(0, &ready); -} - -static s64 call_mkdir(void) { - return (s64)sp_sys_mkdir(0, SP_NULLPTR, 0, sp_zero_s(sp_sys_file_perms_t)); -} - -static s64 call_rmdir(void) { - return (s64)sp_sys_rmdir(0, SP_NULLPTR, 0); -} - -static s64 call_unlink(void) { - return (s64)sp_sys_unlink(0, SP_NULLPTR, 0); -} - -static s64 call_rename(void) { - return (s64)sp_sys_rename(0, SP_NULLPTR, 0, 0, SP_NULLPTR, 0); -} - -static s64 call_link(void) { - return (s64)sp_sys_link(0, SP_NULLPTR, 0, 0, SP_NULLPTR, 0); -} - -static s64 call_symlink(void) { - return (s64)sp_sys_symlink(SP_NULLPTR, 0, 0, SP_NULLPTR, 0); -} - -static s64 call_readlink(void) { - u64 len = 0; - return (s64)sp_sys_readlink(0, SP_NULLPTR, 0, SP_NULLPTR, 0, &len); -} - -static s64 call_get_path_metadata(void) { - return (s64)sp_sys_get_path_metadata(0, SP_NULLPTR, 0, SP_NULLPTR); -} - -static s64 call_get_link_metadata(void) { - return (s64)sp_sys_get_link_metadata(0, SP_NULLPTR, 0, SP_NULLPTR); -} - -static s64 call_get_file_metadata(void) { - return (s64)sp_sys_get_file_metadata(0, SP_NULLPTR); -} - -static s64 call_set_file_perms(void) { - return (s64)sp_sys_set_file_perms(0, SP_NULLPTR, 0, sp_zero_s(sp_sys_file_perms_t)); -} - -static s64 call_clock_gettime(void) { - return (s64)sp_sys_clock_gettime(0, SP_NULLPTR); -} - -static s64 call_nanosleep(void) { - return (s64)sp_sys_nanosleep(SP_NULLPTR, SP_NULLPTR); -} - -static s64 call_futex_wait(void) { - return (s64)sp_sys_futex_wait(SP_NULLPTR, 0, SP_NULLPTR); -} - -static s64 call_futex_wake(void) { - recorded = 0; - sp_sys_futex_wake(SP_NULLPTR); - return recorded; -} - -static s64 call_futex_wake_all(void) { - recorded = 0; - sp_sys_futex_wake_all(SP_NULLPTR); - return recorded; -} - -static s64 call_canonicalize_path(void) { - return sp_sys_canonicalize_path(SP_NULLPTR, 0, SP_NULLPTR, 0); -} - -static s64 call_event_open(void) { - return (s64)sp_sys_event_open(SP_NULLPTR); -} - -static s64 call_event_signal(void) { - return (s64)sp_sys_event_signal(sp_zero_s(sp_sys_event_t)); -} - -static s64 call_event_clear(void) { - return (s64)sp_sys_event_clear(sp_zero_s(sp_sys_event_t)); -} - -static s64 call_wait(void) { - sp_sys_fd_t fds [1] = sp_zero; - return (s64)sp_sys_wait(fds, 0, 0, SP_NULLPTR); -} - -static s64 call_tty_get(void) { - return (s64)sp_sys_tty_get(0, SP_NULLPTR); -} - -static s64 call_tty_set(void) { - return (s64)sp_sys_tty_set(0, SP_NULLPTR); -} - -static s64 call_tty_size(void) { - return (s64)sp_sys_tty_size(0, SP_NULLPTR, SP_NULLPTR); -} - -static s64 call_is_tty(void) { - return (s64)sp_sys_is_tty(0); -} - -static s64 call_tty_ready(void) { - u8 ready = 0; - return (s64)sp_sys_tty_ready(0, &ready); -} - -static s64 call_tty_mode_apply(void) { - return (s64)sp_sys_tty_mode_apply(SP_NULLPTR, SP_NULLPTR, SP_SYS_TTY_MODE_RAW); -} - -static s64 call_tty_use_vt(void) { - return (s64)sp_sys_tty_use_vt(0); -} - -static s64 call_socket_open(void) { - return (s64)sp_sys_socket_open(SP_NULLPTR, sp_zero_s(sp_sys_handle_desc_t)); -} - -static s64 call_socket_bind(void) { - return (s64)sp_sys_socket_bind(0, sp_zero_s(sp_sys_ipv4_t)); -} - -static s64 call_socket_listen(void) { - return (s64)sp_sys_socket_listen(0, 0); -} - -static s64 call_socket_connect(void) { - return (s64)sp_sys_socket_connect(0, sp_zero_s(sp_sys_ipv4_t)); -} - -static s64 call_socket_error(void) { - return (s64)sp_sys_socket_error(0); -} - -static s64 call_socket_accept(void) { - return (s64)sp_sys_socket_accept(0, sp_zero_s(sp_sys_handle_desc_t), SP_NULLPTR); -} - -static s64 call_socket_close(void) { - return (s64)sp_sys_socket_close(0); -} - -static s64 call_socket_recv(void) { - return (s64)sp_sys_socket_recv(0, SP_NULLPTR, 0, SP_NULLPTR); -} - -static s64 call_socket_send(void) { - return (s64)sp_sys_socket_send(0, SP_NULLPTR, 0, SP_NULLPTR); -} - -static s64 call_socket_wait(void) { - return (s64)sp_sys_socket_wait(0, false, 0); -} - -static s64 call_socket_set_nonblocking(void) { - return (s64)sp_sys_socket_set_nonblocking(0); -} - -static s64 call_socket_reuse_addr(void) { - return (s64)sp_sys_socket_reuse_addr(0); -} - -static s64 call_socket_local_port(void) { - return (s64)sp_sys_socket_local_port(0, SP_NULLPTR); -} - -static s64 call_alloc(void) { - return (s64)(uintptr_t)sp_sys_alloc(0); -} - -static s64 call_free(void) { - recorded = 0; - sp_sys_free(SP_NULLPTR, 0); - return recorded; -} - -static s64 call_memcpy(void) { - return (s64)(uintptr_t)sp_sys_memcpy(SP_NULLPTR, SP_NULLPTR, 0); -} - -static s64 call_memmove(void) { - return (s64)(uintptr_t)sp_sys_memmove(SP_NULLPTR, SP_NULLPTR, 0); -} - -static s64 call_memset(void) { - return (s64)(uintptr_t)sp_sys_memset(SP_NULLPTR, 0, 0); -} - -static s64 call_memcmp(void) { - return (s64)sp_sys_memcmp(SP_NULLPTR, SP_NULLPTR, 0); -} - -static s64 call_assert(void) { - recorded = 0; - sp_sys_assert(true); - return recorded; -} - -static s64 call_exit(void) { - recorded = 0; - sp_sys_exit(0); - return recorded; -} - -static s64 call_env(void) { - recorded = 0; - sp_sys_env(SP_NULLPTR, SP_NULLPTR); - return recorded; -} - -static s64 call_lseek(void) { - return sp_sys_lseek(0, 0, 0); -} - -static s64 call_chdir(void) { - return (s64)sp_sys_chdir(SP_NULLPTR, 0); -} - -static s64 call_dir_from_fd(void) { - return (s64)sp_sys_dir_from_fd(0, SP_NULLPTR); -} - -static s64 call_dir_read(void) { - return (s64)sp_sys_dir_read(SP_NULLPTR, SP_NULLPTR); -} - -static s64 call_dir_parse(void) { - return (s64)sp_sys_dir_parse(SP_NULLPTR, SP_NULLPTR, SP_NULLPTR, SP_NULLPTR); -} - -static s64 call_dir_close(void) { - return (s64)sp_sys_dir_close(SP_NULLPTR); -} - -static const test_t tests [] = { - { "read", call_read, 69 }, - { "write", call_write, 69 }, - { "pread", call_pread, 69 }, - { "pwrite", call_pwrite, 69 }, - { "transfer", call_transfer, 69 }, - { "transfer_positional", call_transfer_positional, 69 }, - { "get_root", call_get_root, 69 }, - { "get_exe_path", call_get_exe_path, 69 }, - { "get_cwd_path", call_get_cwd_path, 69 }, - { "get_storage_path", call_get_storage_path, 69 }, - { "get_config_path", call_get_config_path, 69 }, - { "open", call_open, 69 }, - { "open_dir", call_open_dir, 69 }, - { "close", call_close, 69 }, - { "pipe", call_pipe, 69 }, - { "pipe_ready", call_pipe_ready, 69 }, - { "mkdir", call_mkdir, 69 }, - { "rmdir", call_rmdir, 69 }, - { "unlink", call_unlink, 69 }, - { "rename", call_rename, 69 }, - { "link", call_link, 69 }, - { "symlink", call_symlink, 69 }, - { "readlink", call_readlink, 69 }, - { "get_path_metadata", call_get_path_metadata, 69 }, - { "get_link_metadata", call_get_link_metadata, 69 }, - { "get_file_metadata", call_get_file_metadata, 69 }, - { "set_file_perms", call_set_file_perms, 69 }, - { "clock_gettime", call_clock_gettime, 69 }, - { "nanosleep", call_nanosleep, 69 }, - { "futex_wait", call_futex_wait, 1 }, - { "futex_wake", call_futex_wake, 69 }, - { "futex_wake_all", call_futex_wake_all, 69 }, - { "canonicalize_path", call_canonicalize_path, 69 }, - { "event_open", call_event_open, 69 }, - { "event_signal", call_event_signal, 69 }, - { "event_clear", call_event_clear, 69 }, - { "wait", call_wait, 69 }, - { "tty_get", call_tty_get, 69 }, - { "tty_set", call_tty_set, 69 }, - { "tty_size", call_tty_size, 69 }, - { "is_tty", call_is_tty, 1 }, - { "tty_ready", call_tty_ready, 69 }, - { "tty_mode_apply", call_tty_mode_apply, 69 }, - { "tty_use_vt", call_tty_use_vt, 69 }, - { "socket_open", call_socket_open, 69 }, - { "socket_bind", call_socket_bind, 69 }, - { "socket_listen", call_socket_listen, 69 }, - { "socket_connect", call_socket_connect, 69 }, - { "socket_error", call_socket_error, 69 }, - { "socket_accept", call_socket_accept, 69 }, - { "socket_close", call_socket_close, 69 }, - { "socket_recv", call_socket_recv, 69 }, - { "socket_send", call_socket_send, 69 }, - { "socket_wait", call_socket_wait, 69 }, - { "socket_set_nonblocking", call_socket_set_nonblocking, 69 }, - { "socket_reuse_addr", call_socket_reuse_addr, 69 }, - { "socket_local_port", call_socket_local_port, 69 }, - { "alloc", call_alloc, 69 }, - { "free", call_free, 69 }, - { "memcpy", call_memcpy, 69 }, - { "memmove", call_memmove, 69 }, - { "memset", call_memset, 69 }, - { "memcmp", call_memcmp, 69 }, - { "assert", call_assert, 69 }, - { "exit", call_exit, 69 }, - { "env", call_env, 69 }, - { "lseek", call_lseek, 69 }, - { "chdir", call_chdir, 69 }, - { "dir_from_fd", call_dir_from_fd, 69 }, - { "dir_read", call_dir_read, 69 }, - { "dir_parse", call_dir_parse, 69 }, - { "dir_close", call_dir_close, 69 }, -}; - -static sp_err_t run(sp_test_t* t, test_t* c) { - const sp_sys_vtable_t* old = sp_sys_set_vtable(&mock); - s64 got = c->call(); - const sp_sys_vtable_t* swapped = sp_sys_set_vtable(old); - - sp_expect(t, old == &sp_sys_vtable_platform); - sp_expect(t, swapped == &mock); - sp_expect_eq(t, got, c->expect); - return SP_OK; -} - -sp_test_each_fn(sys, vtable, test_t, tests, run, .serial = true); diff --git a/test/test/dir.c b/test/test/dir.c index abe6e192..49ac2d7b 100644 --- a/test/test/dir.c +++ b/test/test/dir.c @@ -35,13 +35,14 @@ sp_test_each(runner, dir, dir_case_t, dir_cases) { sp_test_skip_on_freestanding(); sp_mem_t mem = sp_test_arena(t); - sp_str_t sandbox = sp_test_dir(t); - sp_str_t exe = sp_fs_get_exe_path(mem); + sp_path_t sandbox = sp_test_dir(t); + sp_str_t exe = sp_zero; + sp_must_ok(t, sp_fs_get_exe_path(mem, &exe)); sp_ps_config_t config = { .command = exe, .args = { sp_str_lit("child"), sp_str_lit("--filter"), sp_str_lit("child.pass"), sp_str_lit("--keep"), sp_str_lit("always") }, - .cwd = sandbox, + .cwd = sandbox.sub, .env = { .extra = { { .key = sp_str_lit("SP_TEST_DIR"), .value = sp_cstr_as_str(it->env ? it->env : "") }, @@ -54,7 +55,7 @@ sp_test_each(runner, dir, dir_case_t, dir_cases) { }; if (it->flag) { sp_ps_config_add_arg(mem, &config, sp_str_lit("--dir")); - sp_ps_config_add_arg(mem, &config, sp_fs_join_path(mem, sandbox, sp_cstr_as_str(it->flag))); + sp_ps_config_add_arg(mem, &config, sp_cstr_as_str(it->flag)); } sp_ps_output_t out = sp_ps_run(mem, config); @@ -62,13 +63,13 @@ sp_test_each(runner, dir, dir_case_t, dir_cases) { sp_must_ok(t, out.error); sp_must_eq(t, out.status.exit_code, 0); - sp_str_t root = sp_fs_join_path(mem, sandbox, sp_cstr_as_str(it->expect)); - sp_da(sp_fs_entry_t) runs = SP_NULLPTR; - sp_fs_collect(mem, sp_fs_join_path(mem, root, sp_fs_get_stem(exe)), &runs); - sp_must_eq(t, sp_da_size(runs), (u64)1); + sp_str_t runs = sp_fs_join_path(mem, sp_cstr_as_str(it->expect), sp_fs_get_stem(exe)); + sp_da(sp_str_t) names = sp_da_new(mem, sp_str_t); + sp_fs_for(mem, sp_path_join(mem, sandbox, runs), walk) sp_da_push(names, sp_str_copy(mem, walk.entry.name)); + sp_must_eq(t, sp_da_size(names), (u64)1); - sp_str_t dir = sp_fs_join_path(mem, runs[0].path, sp_str_lit("child.pass")); - sp_expect(t, sp_fs_is_file(sp_fs_join_path(mem, dir, sp_str_lit("A")))); + sp_str_t dir = sp_fs_join_path(mem, sp_fs_join_path(mem, runs, names[0]), sp_str_lit("child.pass")); + sp_expect(t, sp_fs_is_file_at(sp_path_join(mem, sandbox, sp_fs_join_path(mem, dir, sp_str_lit("A"))))); sp_expect(t, sp_str_contains(out.out, sp_test_format(t, "\n dir {}\n", sp_fmt_str(dir)))); return SP_OK; } diff --git a/test/test/golden.c b/test/test/golden.c new file mode 100644 index 00000000..32b221c0 --- /dev/null +++ b/test/test/golden.c @@ -0,0 +1,150 @@ +#include "sp/sp_test.h" + +typedef struct { + const c8* status; + s32 exit_code; + const c8* golden; + const c8* actual; +} expect_t; + +typedef struct { + const c8* name; + const c8* golden; + const c8* cwd; + const c8* root; + const c8* env; + bool update; + bool absolute; + expect_t expect; +} test_t; + +static const test_t tests [] = { + { + .name = "match", + .golden = "A", + .root = ".", + .expect = { .status = "ok", .golden = "A" }, + }, + { + .name = "mismatch", + .golden = "B", + .root = ".", + .expect = { .status = "failed", .exit_code = 1, .golden = "B", .actual = "A" }, + }, + { + .name = "missing", + .root = ".", + .expect = { .status = "failed", .exit_code = 1 }, + }, + { + .name = "update", + .root = ".", + .update = true, + .expect = { .status = "updated", .golden = "A" }, + }, + { + .name = "update_clears_actual", + .golden = "B", + .root = ".", + .update = true, + .expect = { .status = "updated", .golden = "A" }, + }, + { + .name = "env_root", + .golden = "A", + .env = ".", + .expect = { .status = "ok", .golden = "A" }, + }, + { + .name = "flag_over_env", + .golden = "A", + .cwd = "D", + .env = "nowhere", + .root = "..", + .expect = { .status = "ok", .golden = "A" }, + }, + { + .name = "search_from_cwd", + .golden = "A", + .cwd = "D", + .expect = { .status = "ok", .golden = "A" }, + }, + { + .name = "search_walks_up", + .golden = "A", + .cwd = "D/E/F", + .expect = { .status = "ok", .golden = "A" }, + }, + { + .name = "absolute_file", + .golden = "A", + .cwd = "D", + .absolute = true, + .expect = { .status = "ok", .golden = "A" }, + }, +}; + +sp_test_each(runner, golden, test_t, tests) { + sp_test_skip_on_wasm(); + sp_test_skip_on_freestanding(); + + if (it->absolute) sp_test_skip_without_absolute_names(); + + sp_mem_t mem = sp_test_arena(t); + sp_path_t sandbox = sp_test_dir(t); + sp_str_t exe = sp_zero; + sp_must_ok(t, sp_fs_get_exe_path(mem, &exe)); + + sp_path_t src = sp_path_join(mem, sandbox, sp_str_lit("S/main.c")); + sp_path_t golden = sp_path_join(mem, sandbox, sp_str_lit("S/G")); + sp_path_t actual = sp_path_join(mem, sandbox, sp_str_lit("S/G.actual")); + sp_must_ok(t, sp_fs_create_dir_at(sp_path_parent(mem, src))); + sp_must_ok(t, sp_fs_create_file_at(src)); + sp_str_t file = sp_str_lit("S/main.c"); + if (it->absolute) sp_must_ok(t, sp_fs_canonicalize_path_at(mem, src, &file)); + if (it->golden) sp_must_ok(t, sp_fs_create_file_cstr_at(golden, it->golden)); + if (it->update) sp_must_ok(t, sp_fs_create_file_cstr_at(actual, "stale")); + + sp_str_t cwd = it->cwd ? sp_fs_join_path(mem, sandbox.sub, sp_cstr_as_str(it->cwd)) : sandbox.sub; + sp_must_ok(t, sp_fs_create_dir_at(sp_path(sandbox.dir, cwd))); + + sp_ps_config_t config = { + .command = exe, + .args = { sp_str_lit("child"), sp_str_lit("--filter"), sp_str_lit("child.golden"), sp_str_lit("--dir"), sp_str_lit("T") }, + .cwd = cwd, + .env = { + .extra = { + { .key = sp_str_lit("SP_TEST_GOLDEN_ROOT"), .value = sp_cstr_as_str(it->env ? it->env : "") }, + { .key = sp_str_lit("SP_TEST_CHILD_FILE"), .value = file }, + }, + }, + .io = { + .in = { .mode = SP_PS_IO_MODE_NULL }, + .err = { .mode = SP_PS_IO_MODE_NULL }, + }, + }; + if (it->root) { + sp_ps_config_add_arg(mem, &config, sp_str_lit("--golden-root")); + sp_ps_config_add_arg(mem, &config, sp_cstr_as_str(it->root)); + } + if (it->update) sp_ps_config_add_arg(mem, &config, sp_str_lit("--update")); + + sp_ps_output_t out = sp_ps_run(mem, config); + sp_test_kv(t, "stdout", out.out); + sp_must_ok(t, out.error); + sp_expect_eq(t, out.status.exit_code, it->expect.exit_code); + sp_expect(t, sp_str_contains(out.out, sp_test_format(t, "\nchild.golden {} ", sp_fmt_cstr(it->expect.status)))); + + sp_str_t content = sp_zero; + sp_expect_eq(t, sp_fs_exists_at(golden), it->expect.golden != SP_NULLPTR); + if (it->expect.golden) { + sp_expect_ok(t, sp_io_read_file_at(mem, golden, &content)); + sp_expect_str_eq_c(t, content, it->expect.golden); + } + sp_expect_eq(t, sp_fs_exists_at(actual), it->expect.actual != SP_NULLPTR); + if (it->expect.actual) { + sp_expect_ok(t, sp_io_read_file_at(mem, actual, &content)); + sp_expect_str_eq_c(t, content, it->expect.actual); + } + return SP_OK; +} diff --git a/test/test/keep.c b/test/test/keep.c index 72d2b527..9f77ce7a 100644 --- a/test/test/keep.c +++ b/test/test/keep.c @@ -115,12 +115,14 @@ sp_test_each(runner, keep, keep_case_t, keep_cases) { sp_test_skip_on_freestanding(); sp_mem_t mem = sp_test_arena(t); - sp_str_t sandbox = sp_test_dir(t); - sp_str_t exe = sp_fs_get_exe_path(mem); + sp_path_t sandbox = sp_test_dir(t); + sp_str_t exe = sp_zero; + sp_must_ok(t, sp_fs_get_exe_path(mem, &exe)); sp_ps_config_t config = { .command = exe, - .args = { sp_str_lit("child"), sp_str_lit("--filter"), sp_cstr_as_str(it->test), sp_str_lit("--dir"), sandbox }, + .args = { sp_str_lit("child"), sp_str_lit("--filter"), sp_cstr_as_str(it->test), sp_str_lit("--dir"), sp_str_lit("T") }, + .cwd = sandbox.sub, .env = { .extra = { { .key = sp_str_lit("SP_TEST_KEEP"), .value = sp_cstr_as_str(it->env ? it->env : "") }, @@ -142,18 +144,19 @@ sp_test_each(runner, keep, keep_case_t, keep_cases) { sp_expect_eq(t, out.status.exit_code, it->expect.exit_code); sp_expect(t, sp_str_contains(out.out, sp_test_format(t, "\n{} {} ", sp_fmt_cstr(it->test), sp_fmt_cstr(it->expect.status)))); - sp_da(sp_fs_entry_t) runs = SP_NULLPTR; - sp_fs_collect(mem, sp_fs_join_path(mem, sandbox, sp_fs_get_stem(exe)), &runs); + sp_str_t runs = sp_fs_join_path(mem, sp_str_lit("T"), sp_fs_get_stem(exe)); + sp_da(sp_str_t) names = sp_da_new(mem, sp_str_t); + sp_fs_for(mem, sp_path_join(mem, sandbox, runs), walk) sp_da_push(names, sp_str_copy(mem, walk.entry.name)); if (!it->expect.kept) { - sp_expect_eq(t, sp_da_size(runs), (u64)0); + sp_expect_eq(t, sp_da_size(names), (u64)0); sp_expect(t, !sp_str_contains(out.out, sp_str_lit("\n dir "))); return SP_OK; } - sp_must_eq(t, sp_da_size(runs), (u64)1); - sp_str_t dir = sp_fs_join_path(mem, runs[0].path, sp_cstr_as_str(it->test)); - sp_expect(t, sp_fs_is_file(sp_fs_join_path(mem, dir, sp_str_lit("A")))); + sp_must_eq(t, sp_da_size(names), (u64)1); + sp_str_t dir = sp_fs_join_path(mem, sp_fs_join_path(mem, runs, names[0]), sp_cstr_as_str(it->test)); + sp_expect(t, sp_fs_is_file_at(sp_path_join(mem, sandbox, sp_fs_join_path(mem, dir, sp_str_lit("A"))))); sp_expect(t, sp_str_contains(out.out, sp_test_format(t, "\n dir {}\n", sp_fmt_str(dir)))); return SP_OK; } diff --git a/test/test/main.c b/test/test/main.c index 30b91b65..03a45261 100644 --- a/test/test/main.c +++ b/test/test/main.c @@ -73,6 +73,7 @@ static sp_err_t child_pass(sp_test_t* t); static sp_err_t child_fail(sp_test_t* t); static sp_err_t child_leak(sp_test_t* t); static sp_err_t child_skip(sp_test_t* t); +static sp_err_t child_golden(sp_test_t* t); static sp_err_t child_each(sp_test_t* t, const void* arg); static const sp_test_entry_t child_entries [] = { @@ -96,6 +97,10 @@ static const sp_test_entry_t child_entries [] = { .kind = SP_TEST_ENTRY_TEST, .decl = { .suite = "child", .name = "declared", .kind = SP_TEST_DECL_FN, .fn = child_pass, .keep = true }, }, + { + .kind = SP_TEST_ENTRY_TEST, + .decl = { .suite = "child", .name = "golden", .kind = SP_TEST_DECL_FN, .fn = child_golden }, + }, { .kind = SP_TEST_ENTRY_TEST, .decl = { @@ -117,8 +122,8 @@ static const sp_test_entry_t child_entries [] = { }; static sp_err_t child_touch(sp_test_t* t) { - sp_str_t path = sp_fs_join_path(sp_test_arena(t), sp_test_dir(t), sp_str_lit("A")); - return sp_fs_create_file_cstr(path, "A"); + sp_path_t path = sp_path_join(sp_test_arena(t), sp_test_dir(t), sp_str_lit("A")); + return sp_fs_create_file_cstr_at(path, "A"); } static sp_err_t child_pass(sp_test_t* t) { @@ -143,6 +148,11 @@ static sp_err_t child_skip(sp_test_t* t) { return sp_test_skip(t, "S"); } +static sp_err_t child_golden(sp_test_t* t) { + sp_test_golden(t, sp_str_lit("G"), sp_str_lit("A"), sp_os_env_get(sp_str_lit("SP_TEST_CHILD_FILE")), (u32)__LINE__); + return SP_OK; +} + static sp_err_t child_each(sp_test_t* t, const void* arg) { SP_UNUSED(arg); sp_must_ok(t, child_touch(t)); diff --git a/test/test/prune.c b/test/test/prune.c index b2375b11..78e1c96d 100644 --- a/test/test/prune.c +++ b/test/test/prune.c @@ -69,28 +69,30 @@ sp_test_each(runner, prune, prune_case_t, prune_cases) { sp_test_skip_on_freestanding(); sp_mem_t mem = sp_test_arena(t); - sp_str_t sandbox = sp_test_dir(t); - sp_str_t exe = sp_fs_get_exe_path(mem); - sp_str_t runs = sp_fs_join_path(mem, sandbox, sp_fs_get_stem(exe)); + sp_path_t sandbox = sp_test_dir(t); + sp_str_t exe = sp_zero; + sp_must_ok(t, sp_fs_get_exe_path(mem, &exe)); + sp_path_t runs = sp_path_join(mem, sandbox, sp_fs_join_path(mem, sp_str_lit("T"), sp_fs_get_stem(exe))); sp_tm_epoch_t now = sp_tm_now_epoch(); - sp_must_ok(t, sp_fs_create_dir(runs)); - sp_str_t paths [PRUNE_MAX] = sp_zero; + sp_must_ok(t, sp_fs_create_dir_at(runs)); + sp_path_t paths [PRUNE_MAX] = sp_zero; sp_carr_for(it->seeds, at) { const prune_seed_t* seed = &it->seeds[at]; if (!seed->id) break; - paths[at] = sp_fs_join_path(mem, runs, prune_run_name(mem, now, seed->age_s)); + paths[at] = sp_path_join(mem, runs, prune_run_name(mem, now, seed->age_s)); if (seed->file) { - sp_must_ok(t, sp_fs_create_file_cstr(paths[at], "A")); + sp_must_ok(t, sp_fs_create_file_cstr_at(paths[at], "A")); } else { - sp_must_ok(t, sp_fs_create_dir(paths[at])); + sp_must_ok(t, sp_fs_create_dir_at(paths[at])); } } sp_ps_output_t out = sp_ps_run(mem, (sp_ps_config_t) { .command = exe, - .args = { sp_str_lit("child"), sp_str_lit("--filter"), sp_str_lit("child.pass"), sp_str_lit("--dir"), sandbox }, + .args = { sp_str_lit("child"), sp_str_lit("--filter"), sp_str_lit("child.pass"), sp_str_lit("--dir"), sp_str_lit("T") }, + .cwd = sandbox.sub, .env = { .extra = { { .key = sp_str_lit("SP_TEST_KEEP"), .value = sp_str_lit("never") }, @@ -108,12 +110,12 @@ sp_test_each(runner, prune, prune_case_t, prune_cases) { sp_da(sp_str_t) survivors = sp_da_new(mem, sp_str_t); sp_carr_for(it->seeds, at) { if (!it->seeds[at].id) break; - if (sp_fs_exists(paths[at])) sp_da_push(survivors, sp_cstr_as_str(it->seeds[at].id)); + if (sp_fs_exists_at(paths[at])) sp_da_push(survivors, sp_cstr_as_str(it->seeds[at].id)); } sp_expect_strs_eq(t, survivors, sp_da_size(survivors), it->expect); - sp_da(sp_fs_entry_t) entries = SP_NULLPTR; - sp_fs_collect(mem, runs, &entries); - sp_expect_eq(t, sp_da_size(entries), sp_da_size(survivors)); + u64 entries = 0; + sp_fs_for(mem, runs, walk) entries++; + sp_expect_eq(t, entries, sp_da_size(survivors)); return SP_OK; } diff --git a/test/test/resolve.c b/test/test/resolve.c deleted file mode 100644 index 5c32770a..00000000 --- a/test/test/resolve.c +++ /dev/null @@ -1,49 +0,0 @@ -#include "sp/sp_test.h" - -typedef struct { - const c8* name; - const c8* cwd; - const c8* exe_dir; - const c8* expect [16]; -} resolve_case_t; - -static const resolve_case_t resolve_cases [] = { - { - .name = "disjoint", - .cwd = "/a/b", - .exe_dir = "/x/y", - .expect = { "/a/b", "/a", "/x/y", "/x" }, - }, - { - .name = "overlap", - .cwd = "/x/repo", - .exe_dir = "/x/repo/build/debug", - .expect = { "/x/repo", "/x", "/x/repo/build/debug", "/x/repo/build" }, - }, - { - .name = "windows", - .cwd = "C:\\repo", - .exe_dir = "C:\\repo\\build", - .expect = { "C:/repo", "C:", "C:/repo/build" }, - }, - { - .name = "trailing", - .cwd = "/a/b/", - .exe_dir = "", - .expect = { "/a/b", "/a" }, - }, - { - .name = "empty", - .cwd = "", - .exe_dir = "/x", - .expect = { "/x" }, - }, -}; - -sp_test_each(golden, resolve, resolve_case_t, resolve_cases) { - sp_da(sp_str_t) roots = sp_test_resolve_roots(sp_test_arena(t), - sp_cstr_as_str(it->cwd), sp_cstr_as_str(it->exe_dir)); - - sp_must_strs_eq(t, roots, sp_da_size(roots), it->expect); - return SP_OK; -} diff --git a/test/tools/test.h b/test/tools/test.h index b2728e5c..5bfaa964 100644 --- a/test/tools/test.h +++ b/test/tools/test.h @@ -162,7 +162,9 @@ static sp_str_t sp_test_file_manager_get_top_level(sp_mem_t a) { return sp_test_file_manager_top_level; } - sp_str_t tmp = sp_fs_join_path(a, sp_fs_get_cwd(a), sp_str_lit(".tmp")); + sp_str_t cwd = sp_zero; + sp_fs_get_cwd_path(a, &cwd); + sp_str_t tmp = sp_fs_join_path(a, cwd, sp_str_lit(".tmp")); if (!sp_fs_exists(tmp)) { sp_fs_create_dir(tmp); } @@ -178,14 +180,14 @@ static sp_str_t sp_test_file_manager_get_top_level(sp_mem_t a) { // Cache in a long-lived allocator (os) so the result outlives `a`, which is // typically a per-test arena that gets destroyed in cleanup. - sp_test_file_manager_top_level = sp_fs_canonicalize_path(sp_mem_os_new(), root); + sp_fs_canonicalize_path(sp_mem_os_new(), root, &sp_test_file_manager_top_level); return sp_test_file_manager_top_level; } void sp_test_file_manager_init(sp_test_file_manager_t* fs) { fs->arena = sp_mem_arena_new(sp_mem_os_new()); fs->mem = sp_mem_arena_as_allocator(fs->arena); - fs->paths.bin = sp_fs_get_exe_path(fs->mem); + sp_fs_get_exe_path(fs->mem, &fs->paths.bin); fs->paths.test = sp_test_file_manager_get_top_level(fs->mem); if (!sp_fs_exists(fs->paths.test)) { diff --git a/tools/sp.c b/tools/sp.c index e2e7b3e4..28b9719f 100644 --- a/tools/sp.c +++ b/tools/sp.c @@ -160,7 +160,7 @@ static sp_da(const target_t*) target_resolve(sp_mem_t mem, sp_str_t query) { static sp_da(sp_str_t) tests_discover(sp_mem_t mem, sp_str_t dir, sp_str_t ext) { sp_da(sp_str_t) names = sp_da_new(mem, sp_str_t); - sp_fs_for(mem, dir, it) { + sp_fs_for(mem, sp_path_from_str(dir), it) { sp_str_t name = it.entry.name; if (!sp_str_empty(ext)) { if (!sp_str_ends_with(name, ext)) continue; @@ -383,7 +383,9 @@ static test_t test_init(sp_mem_t mem, tool_t* tool, const target_t* target, sp_s break; } case EXEC_DOCKER: { - sp_str_t mount = sp_fmt(mem, "{}:/sp", sp_fmt_str(sp_fs_get_cwd(mem))).value; + sp_str_t cwd = sp_zero; + sp_fs_get_cwd_path(mem, &cwd); + sp_str_t mount = sp_fmt(mem, "{}:/sp", sp_fmt_str(cwd)).value; sp_str_t path = sp_fmt(mem, "/sp/{}", sp_fmt_str(rel)).value; test.display = sp_fmt(mem, "docker run --rm --platform linux/arm64 -v {} -w /sp debian:stable-slim {}", sp_fmt_str(mount), sp_fmt_str(path)).value; test.config = (sp_ps_config_t) { diff --git a/tools/windows/sp/spn/amalgamation.vcxproj b/tools/windows/sp/spn/amalgamation.vcxproj index 376ef584..26b715a1 100644 --- a/tools/windows/sp/spn/amalgamation.vcxproj +++ b/tools/windows/sp/spn/amalgamation.vcxproj @@ -18,7 +18,7 @@ amalgamation 10.0 amalgamation - ..\..\..\..\test\main.c;..\..\..\..\test\test\axis.c;..\..\..\..\test\test\filter.c;..\..\..\..\test\test\matrix.c;..\..\..\..\test\test\resolve.c;..\..\..\..\test\test\sweep.c;..\..\..\..\test\test\tracking.c;..\..\..\..\test\sys\sys.c;..\..\..\..\test\sys\open.c;..\..\..\..\test\sys\path.c;..\..\..\..\test\sys\iter.c;..\..\..\..\test\sys\futex.c;..\..\..\..\test\sys\fd.c;..\..\..\..\test\sys\nanosleep.c;..\..\..\..\test\sys\timestamps.c;..\..\..\..\test\sys\perms.c;..\..\..\..\test\sys\self_path.c;..\..\..\..\test\sys\pipe\data.c;..\..\..\..\test\sys\pipe\blocking.c;..\..\..\..\test\sys\transfer.c;..\..\..\..\test\sys\readlink.c;..\..\..\..\test\sys\socket\data.c;..\..\..\..\test\sys\socket\listen.c;..\..\..\..\test\sys\socket\desc.c;..\..\..\..\test\sys\tty\contract.c;..\..\..\..\test\sys\tty\mode.c;..\..\..\..\test\sys\vtable.c;..\..\..\..\test\sys\wait\event.c;..\..\..\..\test\sys\wait\wait.c;..\..\..\..\test\sys\inherit.c;..\..\..\..\test\fs\dir\*.c;..\..\..\..\test\fs\it\*.c;..\..\..\..\test\fs\wtf8\*.c;..\..\..\..\test\fs\*.c;..\..\..\..\test\fs\windows\nt_path.c;..\..\..\..\test\msvc\*.c;..\..\..\..\test\mem\uninitialized.c;..\..\..\..\test\atomic.c;..\..\..\..\test\sync\*.c;..\..\..\..\test\sync\os\*.c + ..\..\..\..\test\main.c;..\..\..\..\test\test\axis.c;..\..\..\..\test\test\filter.c;..\..\..\..\test\test\matrix.c;..\..\..\..\test\test\sweep.c;..\..\..\..\test\test\tracking.c;..\..\..\..\test\sys\sys.c;..\..\..\..\test\sys\open.c;..\..\..\..\test\sys\path.c;..\..\..\..\test\sys\futex.c;..\..\..\..\test\sys\fd.c;..\..\..\..\test\sys\fd_path.c;..\..\..\..\test\sys\nanosleep.c;..\..\..\..\test\sys\timestamps.c;..\..\..\..\test\sys\perms.c;..\..\..\..\test\sys\self_path.c;..\..\..\..\test\sys\pipe\data.c;..\..\..\..\test\sys\pipe\blocking.c;..\..\..\..\test\sys\transfer.c;..\..\..\..\test\sys\readlink.c;..\..\..\..\test\sys\socket\data.c;..\..\..\..\test\sys\socket\listen.c;..\..\..\..\test\sys\socket\desc.c;..\..\..\..\test\sys\tty\contract.c;..\..\..\..\test\sys\tty\mode.c;..\..\..\..\test\sys\wait\event.c;..\..\..\..\test\sys\wait\wait.c;..\..\..\..\test\sys\inherit.c;..\..\..\..\test\fs\dir\*.c;..\..\..\..\test\fs\it\*.c;..\..\..\..\test\fs\remove\*.c;..\..\..\..\test\fs\wtf8\*.c;..\..\..\..\test\fs\*.c;..\..\..\..\test\fs\windows\nt_path.c;..\..\..\..\test\msvc\*.c;..\..\..\..\test\mem\uninitialized.c;..\..\..\..\test\atomic.c;..\..\..\..\test\sync\*.c;..\..\..\..\test\sync\os\*.c ..\..\..\..\sp.h;..\..\..\..\sp\sp_test.h false diff --git a/tools/windows/sp/spn/fs.vcxproj b/tools/windows/sp/spn/fs.vcxproj index 1d92a2fa..dd27df88 100644 --- a/tools/windows/sp/spn/fs.vcxproj +++ b/tools/windows/sp/spn/fs.vcxproj @@ -18,7 +18,7 @@ fs 10.0 fs - ..\..\..\..\test\main.c;..\..\..\..\test\fs\dir\*.c;..\..\..\..\test\fs\it\*.c;..\..\..\..\test\fs\wtf8\*.c;..\..\..\..\test\fs\*.c;..\..\..\..\test\fs\windows\nt_path.c + ..\..\..\..\test\main.c;..\..\..\..\test\fs\dir\*.c;..\..\..\..\test\fs\it\*.c;..\..\..\..\test\fs\remove\*.c;..\..\..\..\test\fs\wtf8\*.c;..\..\..\..\test\fs\*.c;..\..\..\..\test\fs\windows\nt_path.c ..\..\..\..\sp.h;..\..\..\..\sp\sp_test.h false diff --git a/tools/windows/sp/spn/sys.vcxproj b/tools/windows/sp/spn/sys.vcxproj index 0485588b..b144d8c3 100644 --- a/tools/windows/sp/spn/sys.vcxproj +++ b/tools/windows/sp/spn/sys.vcxproj @@ -18,7 +18,7 @@ sys 10.0 sys - ..\..\..\..\test\main.c;..\..\..\..\test\sys\sys.c;..\..\..\..\test\sys\open.c;..\..\..\..\test\sys\path.c;..\..\..\..\test\sys\iter.c;..\..\..\..\test\sys\futex.c;..\..\..\..\test\sys\fd.c;..\..\..\..\test\sys\nanosleep.c;..\..\..\..\test\sys\timestamps.c;..\..\..\..\test\sys\perms.c;..\..\..\..\test\sys\self_path.c;..\..\..\..\test\sys\pipe\data.c;..\..\..\..\test\sys\pipe\blocking.c;..\..\..\..\test\sys\transfer.c;..\..\..\..\test\sys\readlink.c;..\..\..\..\test\sys\socket\data.c;..\..\..\..\test\sys\socket\listen.c;..\..\..\..\test\sys\socket\desc.c;..\..\..\..\test\sys\tty\contract.c;..\..\..\..\test\sys\tty\mode.c;..\..\..\..\test\sys\vtable.c;..\..\..\..\test\sys\wait\event.c;..\..\..\..\test\sys\wait\wait.c;..\..\..\..\test\sys\inherit.c + ..\..\..\..\test\main.c;..\..\..\..\test\sys\sys.c;..\..\..\..\test\sys\open.c;..\..\..\..\test\sys\path.c;..\..\..\..\test\sys\futex.c;..\..\..\..\test\sys\fd.c;..\..\..\..\test\sys\fd_path.c;..\..\..\..\test\sys\nanosleep.c;..\..\..\..\test\sys\timestamps.c;..\..\..\..\test\sys\perms.c;..\..\..\..\test\sys\self_path.c;..\..\..\..\test\sys\pipe\data.c;..\..\..\..\test\sys\pipe\blocking.c;..\..\..\..\test\sys\transfer.c;..\..\..\..\test\sys\readlink.c;..\..\..\..\test\sys\socket\data.c;..\..\..\..\test\sys\socket\listen.c;..\..\..\..\test\sys\socket\desc.c;..\..\..\..\test\sys\tty\contract.c;..\..\..\..\test\sys\tty\mode.c;..\..\..\..\test\sys\wait\event.c;..\..\..\..\test\sys\wait\wait.c;..\..\..\..\test\sys\inherit.c ..\..\..\..\sp.h;..\..\..\..\sp\sp_test.h false diff --git a/tools/wip/embed.c b/tools/wip/embed.c index 66366afb..6d5ea21f 100644 --- a/tools/wip/embed.c +++ b/tools/wip/embed.c @@ -32,18 +32,6 @@ s32 embed_main(s32 argc, const c8** argv) { sp_str_t out_hdr = sp_str_view(argv[3]); sp_log("scanning {}", sp_fmt_str(src_dir)); - sp_da(sp_fs_entry_t) files; - if (sp_fs_collect_recursive(mem, src_dir, &files)) { - sp_log("could not read {}", sp_fmt_str(src_dir)); - rc = 1; - goto cleanup; - } - if (sp_da_empty(files)) { - sp_log("no files found in {}", sp_fmt_str(src_dir)); - rc = 1; - goto cleanup; - } - sp_elf_t* elf = sp_elf_new_with_null_section(mem); sp_elf_symtab_new(elf); @@ -52,18 +40,13 @@ s32 embed_main(s32 argc, const c8** argv) { sp_da(embed_entry_t) entries = sp_da_new(mem, embed_entry_t); - sp_da_for(files, it) { - sp_fs_entry_t ent = files[it]; - if (ent.kind == SP_FS_KIND_DIR) { - continue; - } - - u32 skip = src_dir.len + 1; - sp_str_t rel_path = sp_str_sub(ent.path, skip, ent.path.len - skip); + sp_fs_it_t files = sp_fs_it_new(mem, src_dir); + while (sp_fs_it_walk(&files)) { + sp_str_t rel_path = sp_str_copy(mem, files.entry.rel); sp_str_t symbol = symbol_from_path(mem, rel_path); sp_str_t content = sp_zero; - sp_io_read_file(mem, ent.path, &content); + sp_io_read_file(mem, files.entry.path, &content); u64 size = content.len; sp_elf_section_t* symtab = sp_elf_find_section_by_name(elf, sp_str_lit(".symtab")); @@ -106,6 +89,17 @@ s32 embed_main(s32 argc, const c8** argv) { .size = size, })); } + sp_fs_it_deinit(&files); + if (files.err) { + sp_log("could not read {}", sp_fmt_str(src_dir)); + rc = 1; + goto cleanup; + } + if (sp_da_empty(entries)) { + sp_log("no files found in {}", sp_fmt_str(src_dir)); + rc = 1; + goto cleanup; + } { sp_str_t file_path = sp_str_view(argv[4]);