Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
b91e280
easy ones
tspader Sep 18, 2026
45dc3b3
delete sp_fs_link
tspader Sep 18, 2026
74e850d
more
tspader Sep 18, 2026
645800a
fuck
tspader Sep 19, 2026
8266078
canonicalize
tspader Sep 19, 2026
4f398ec
2
tspader Sep 22, 2026
075c5e8
fs: trim copy tests
tspader Sep 22, 2026
42f45b7
fs: trim canonicalize tests
tspader Sep 22, 2026
38c367c
fs: test write_atomic
tspader Sep 22, 2026
543a113
sys: delete iter and vtable tests
tspader Sep 22, 2026
59c3b4a
fs: stop relying on parent creation in atomic tests
tspader Sep 22, 2026
38e4428
fs: copy tests for no parent creation and open-first source
tspader Sep 22, 2026
12a203a
fs: atomic tests for no parent creation
tspader Sep 22, 2026
d3bd89a
fs: write_atomic test for no parent creation
tspader Sep 22, 2026
4739169
fs: NT reports a file parent as not found in atomic tests
tspader Sep 22, 2026
c0b8b4a
fs: platform-correct parent-is-file and exclusive symlink copy cases
tspader Sep 22, 2026
f23c61d
more
tspader Sep 23, 2026
41be771
remove_dir
tspader Sep 23, 2026
879219a
remove_dir 2
tspader Sep 23, 2026
3208d31
fs: walker enter/leave stubs
tspader Sep 23, 2026
cafba64
fs: walker design review
tspader Sep 24, 2026
44466c3
fs: walker enter/leave; copy_tree and remove_dir on it
tspader Sep 24, 2026
88b7916
sys: open_dir NO_FOLLOW reports LOOP for symlinks on every platform
tspader Sep 24, 2026
3a05251
fs: walker implementation review
tspader Sep 24, 2026
98db7de
cleanup, fs example
tspader Sep 24, 2026
5f2bc92
sys: raw wasi imports for path and dir syscalls
tspader Sep 24, 2026
5e9403a
sys: file metadata on wasi
tspader Sep 24, 2026
4518028
sys: open and open_dir on wasi
tspader Sep 24, 2026
efd7778
kill posix slash semantics
tspader Sep 24, 2026
16fdc70
cwd stuff
tspader Sep 25, 2026
276a6dc
kek
tspader Sep 26, 2026
34b2148
sys: prestat roots, fd_path on the cwd; fs: prefix matching in path_a…
tspader Sep 26, 2026
c7dca25
fixit
tspader Sep 27, 2026
40cfdeb
crappy fs examples
tspader Sep 27, 2026
7939e14
more basic 1:1 wasi
tspader Sep 27, 2026
c27c364
fs: stub root labels, host kind path helpers
tspader Sep 27, 2026
5e48e1f
fs: tests for host kind helpers, root labels, fd paths
tspader Sep 27, 2026
d2770f0
fs: tighten path tests after design review
tspader Sep 27, 2026
2802b53
fs: host kind path helpers, root labels, verified fd paths
tspader Sep 27, 2026
51c2254
example: one caller shape per fs example
tspader Sep 27, 2026
26f7dc8
small glob/iterator fix
tspader Sep 28, 2026
7816fc5
fs: remove sp_rt.cwd
tspader Sep 28, 2026
1f943f7
fs: sp_path_join
tspader Sep 28, 2026
f2ebcf3
fs: sp_fs_for takes an sp_path_t
tspader Sep 28, 2026
44528bc
cli: completer joins with sp_fs_join_path
tspader Sep 28, 2026
2df633c
sys: sp_err_t for exe/storage/config paths and lseek
tspader Sep 28, 2026
671b955
test: symlink targets are relative to the dir now
tspader Sep 29, 2026
cafe295
run wasm with a preopen
tspader Sep 29, 2026
04a005c
msvc
tspader Sep 29, 2026
8b72c56
symlinks must specify a kind
tspader Sep 29, 2026
df486a7
sp_path_t in tests
tspader Sep 29, 2026
cf3660b
move sp_test to rooted
tspader Sep 29, 2026
a898dad
fix dir/it symbols
tspader Sep 29, 2026
c3d233e
report the test dir with its root label
tspader Sep 29, 2026
a966e15
rooted goldens
tspader Sep 29, 2026
ff49ce7
golden harness tests
tspader Sep 29, 2026
80b5ad7
sys: add dir_it_rewind
tspader Sep 29, 2026
ba77c5f
test: sim lists dirs positionally
tspader Sep 29, 2026
e4c1e44
fs: rescan dirs after removing entries
tspader Sep 29, 2026
f8fcdb2
test: dir rescan restarts listing
tspader Sep 29, 2026
662e4ba
test: remove across shifted listings
tspader Sep 29, 2026
94085bf
create_parent
tspader Sep 29, 2026
515f107
fix remove_dir
tspader Sep 29, 2026
574d537
collect cleanup
tspader Sep 29, 2026
227a185
cleanup
tspader Oct 1, 2026
6dc1f55
fs: stub errors for path getters and canonicalize, pin failing tests
tspader Oct 1, 2026
33a5d0b
fs: remove_dir rmdirs directories it cannot list
tspader Oct 1, 2026
daa3efe
fs: path getters return the sys error
tspader Oct 1, 2026
0a63c8b
fs: canonicalize_path returns its error
tspader Oct 1, 2026
3dfac30
fs: sp_path_parent leaves an empty sub empty
tspader Oct 1, 2026
b29936a
sys: storage and config paths leave sys normalized
tspader Oct 1, 2026
572214f
fs: rename the sp_path_t constructors
tspader Oct 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ else
endif

ifneq (,$(findstring wasm32,$(TRIPLE)))
RUNNER = wasmtime run
RUNNER = wasmtime run --dir $(CURDIR)
endif

LDLIBS_PLATFORM =
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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.
Expand Down
29 changes: 29 additions & 0 deletions example/fs/absolute.c
Original file line number Diff line number Diff line change
@@ -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)
27 changes: 27 additions & 0 deletions example/fs/config.c
Original file line number Diff line number Diff line change
@@ -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)
64 changes: 64 additions & 0 deletions example/fs/iterator.c
Original file line number Diff line number Diff line change
@@ -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)
51 changes: 51 additions & 0 deletions example/fs/ls.c
Original file line number Diff line number Diff line change
@@ -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)
25 changes: 25 additions & 0 deletions example/fs/preopen.c
Original file line number Diff line number Diff line change
@@ -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)
37 changes: 37 additions & 0 deletions example/fs/roots.c
Original file line number Diff line number Diff line change
@@ -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)
49 changes: 49 additions & 0 deletions example/fs/workspace.c
Original file line number Diff line number Diff line change
@@ -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)
5 changes: 3 additions & 2 deletions example/io.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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)));
Expand Down
4 changes: 1 addition & 3 deletions example/ls.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Loading
Loading