Bläddra i källkod

Merge pull request #4716 from laytan/os2-wasi

os/os2: wasi target support
gingerBill 7 månader sedan
förälder
incheckning
223970671f

+ 1 - 0
core/crypto/rand_generic.odin

@@ -5,6 +5,7 @@
 #+build !netbsd
 #+build !darwin
 #+build !js
+#+build !wasi
 package crypto
 
 HAS_RAND_BYTES :: false

+ 13 - 0
core/crypto/rand_wasi.odin

@@ -0,0 +1,13 @@
+package crypto
+
+import "core:fmt"
+import "core:sys/wasm/wasi"
+
+HAS_RAND_BYTES :: true
+
+@(private)
+_rand_bytes :: proc(dst: []byte) {
+	if err := wasi.random_get(dst); err != nil {
+		fmt.panicf("crypto: wasi.random_get failed: %v", err)
+	}
+}

+ 6 - 3
core/os/os2/dir_posix.odin

@@ -39,8 +39,11 @@ _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info
 		}
 
 		n := len(fimpl.name)+1
-		non_zero_resize(&it.impl.fullpath, n+len(sname))
-		n += copy(it.impl.fullpath[n:], sname)
+		if err := non_zero_resize(&it.impl.fullpath, n+len(sname)); err != nil {
+			// Can't really tell caller we had an error, sad.
+			return
+		}
+		copy(it.impl.fullpath[n:], sname)
 
 		fi = internal_stat(stat, string(it.impl.fullpath[:]))
 		ok = true
@@ -60,7 +63,7 @@ _read_directory_iterator_create :: proc(f: ^File) -> (iter: Read_Directory_Itera
 	iter.f = f
 	iter.impl.idx = 0
 
-	iter.impl.fullpath.allocator = file_allocator()
+	iter.impl.fullpath = make([dynamic]byte, 0, len(impl.name)+128, file_allocator()) or_return
 	append(&iter.impl.fullpath, impl.name)
 	append(&iter.impl.fullpath, "/")
 	defer if err != nil { delete(iter.impl.fullpath) }

+ 110 - 0
core/os/os2/dir_wasi.odin

@@ -0,0 +1,110 @@
+#+private
+package os2
+
+import "base:intrinsics"
+import "core:sys/wasm/wasi"
+
+Read_Directory_Iterator_Impl :: struct {
+	fullpath: [dynamic]byte,
+	buf:      []byte,
+	off:      int,
+	idx:      int,
+}
+
+@(require_results)
+_read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info, index: int, ok: bool) {
+	fimpl := (^File_Impl)(it.f.impl)
+
+	buf := it.impl.buf[it.impl.off:]
+
+	index = it.impl.idx
+	it.impl.idx += 1
+
+	for {
+		if len(buf) < size_of(wasi.dirent_t) {
+			return
+		}
+
+		entry := intrinsics.unaligned_load((^wasi.dirent_t)(raw_data(buf)))
+		buf    = buf[size_of(wasi.dirent_t):]
+
+		if len(buf) < int(entry.d_namlen) {
+			// shouldn't be possible.
+			return
+		}
+
+		name := string(buf[:entry.d_namlen])
+		buf = buf[entry.d_namlen:]
+		it.impl.off += size_of(wasi.dirent_t) + int(entry.d_namlen)
+
+		if name == "." || name == ".." {
+			continue
+		}
+
+		n := len(fimpl.name)+1
+		if alloc_err := non_zero_resize(&it.impl.fullpath, n+len(name)); alloc_err != nil {
+			// Can't really tell caller we had an error, sad.
+			return
+		}
+		copy(it.impl.fullpath[n:], name)
+
+		stat, err := wasi.path_filestat_get(__fd(it.f), {}, name)
+		if err != nil {
+			// Can't stat, fill what we have from dirent.
+			stat = {
+				ino      = entry.d_ino,
+				filetype = entry.d_type,
+			}
+		}
+
+		fi = internal_stat(stat, string(it.impl.fullpath[:]))
+		ok = true
+		return
+	}
+}
+
+@(require_results)
+_read_directory_iterator_create :: proc(f: ^File) -> (iter: Read_Directory_Iterator, err: Error) {
+	if f == nil || f.impl == nil {
+		err = .Invalid_File
+		return
+	}
+
+	impl := (^File_Impl)(f.impl)
+	iter.f = f
+
+	buf: [dynamic]byte
+	buf.allocator = file_allocator()
+	defer if err != nil { delete(buf) }
+
+	// NOTE: this is very grug.
+	for {
+		non_zero_resize(&buf, 512 if len(buf) == 0 else len(buf)*2) or_return
+
+		n, _err := wasi.fd_readdir(__fd(f), buf[:], 0)
+		if _err != nil {
+			err = _get_platform_error(_err)
+			return
+		}
+
+		if n < len(buf) {
+			non_zero_resize(&buf, n)
+			break
+		}
+
+		assert(n == len(buf))
+	}
+	iter.impl.buf = buf[:]
+
+	iter.impl.fullpath = make([dynamic]byte, 0, len(impl.name)+128, file_allocator()) or_return
+	append(&iter.impl.fullpath, impl.name)
+	append(&iter.impl.fullpath, "/")
+
+	return
+}
+
+_read_directory_iterator_destroy :: proc(it: ^Read_Directory_Iterator) {
+	delete(it.impl.buf, file_allocator())
+	delete(it.impl.fullpath)
+	it^ = {}
+}

+ 186 - 0
core/os/os2/env_wasi.odin

@@ -0,0 +1,186 @@
+#+private
+package os2
+
+import "base:runtime"
+
+import "core:strings"
+import "core:sync"
+import "core:sys/wasm/wasi"
+
+g_env: map[string]string
+g_env_buf: []byte
+g_env_mutex: sync.RW_Mutex
+g_env_error: Error
+g_env_built: bool
+
+build_env :: proc() -> (err: Error) {
+	if g_env_built || g_env_error != nil {
+		return g_env_error
+	}
+
+	sync.guard(&g_env_mutex)
+
+	if g_env_built || g_env_error != nil {
+		return g_env_error
+	}
+
+	defer if err != nil {
+		g_env_error = err
+	}
+
+	num_envs, size_of_envs, _err := wasi.environ_sizes_get()
+	if _err != nil {
+		return _get_platform_error(_err)
+	}
+
+	g_env = make(map[string]string, num_envs, file_allocator()) or_return
+	defer if err != nil { delete(g_env) }
+
+	g_env_buf = make([]byte, size_of_envs, file_allocator()) or_return
+	defer if err != nil { delete(g_env_buf, file_allocator()) }
+
+	TEMP_ALLOCATOR_GUARD()
+
+	envs := make([]cstring, num_envs, temp_allocator()) or_return
+
+	_err = wasi.environ_get(raw_data(envs), raw_data(g_env_buf))
+	if _err != nil {
+		return _get_platform_error(_err)
+	}
+
+	for env in envs {
+		key, _, value := strings.partition(string(env), "=")
+		g_env[key] = value
+	}
+
+	g_env_built = true
+	return
+}
+
+delete_string_if_not_original :: proc(str: string) {
+	start := uintptr(raw_data(g_env_buf))
+	end   := start + uintptr(len(g_env_buf))
+	ptr   := uintptr(raw_data(str))
+	if ptr < start || ptr > end {
+		delete(str, file_allocator())
+	}
+}
+
+@(require_results)
+_lookup_env :: proc(key: string, allocator: runtime.Allocator) -> (value: string, found: bool) {
+	if err := build_env(); err != nil {
+		return
+	}
+
+	sync.shared_guard(&g_env_mutex)
+
+	value = g_env[key] or_return
+	value, _ = clone_string(value, allocator)
+	return
+}
+
+@(require_results)
+_set_env :: proc(key, value: string) -> bool {
+	if err := build_env(); err != nil {
+		return false
+	}
+
+	sync.guard(&g_env_mutex)
+
+	key_ptr, value_ptr, just_inserted, err := map_entry(&g_env, key)
+	if err != nil {
+		return false
+	}
+
+	alloc_err: runtime.Allocator_Error
+
+	if just_inserted {
+		key_ptr^, alloc_err = clone_string(key, file_allocator())
+		if alloc_err != nil {
+			delete_key(&g_env, key)
+			return false
+		}
+
+		value_ptr^, alloc_err = clone_string(value, file_allocator())
+		if alloc_err != nil {
+			delete_key(&g_env, key)
+			delete(key_ptr^, file_allocator())
+			return false
+		}
+
+		return true
+	}
+
+	delete_string_if_not_original(value_ptr^)
+
+	value_ptr^, alloc_err = clone_string(value, file_allocator())
+	if alloc_err != nil {
+		delete_key(&g_env, key)
+		return false
+	}
+
+	return true
+}
+
+@(require_results)
+_unset_env :: proc(key: string) -> bool {
+	if err := build_env(); err != nil {
+		return false
+	}
+
+	sync.guard(&g_env_mutex)
+
+	dkey, dval := delete_key(&g_env, key)
+	delete_string_if_not_original(dkey)
+	delete_string_if_not_original(dval)
+	return true
+}
+
+_clear_env :: proc() {
+	sync.guard(&g_env_mutex)
+
+	for k, v in g_env {
+		delete_string_if_not_original(k)
+		delete_string_if_not_original(v)
+	}
+
+	delete(g_env_buf, file_allocator())
+	g_env_buf = {}
+
+	clear(&g_env)
+
+	g_env_built = true
+}
+
+@(require_results)
+_environ :: proc(allocator: runtime.Allocator) -> []string {
+	if err := build_env(); err != nil {
+		return nil
+	}
+
+	sync.shared_guard(&g_env_mutex)
+
+	envs, alloc_err := make([]string, len(g_env), allocator)
+	if alloc_err != nil {
+		return nil
+	}
+
+	defer if alloc_err != nil {
+		for env in envs {
+			delete(env, allocator)
+		}
+		delete(envs, allocator)
+	}
+
+	i: int
+	for k, v in g_env {
+		defer i += 1
+
+		envs[i], alloc_err = concatenate({k, "=", v}, allocator)
+		if alloc_err != nil {
+			return nil
+		}
+	}
+
+	return envs
+}

+ 47 - 0
core/os/os2/errors_wasi.odin

@@ -0,0 +1,47 @@
+#+private
+package os2
+
+import "base:runtime"
+
+import "core:slice"
+import "core:sys/wasm/wasi"
+
+_Platform_Error :: wasi.errno_t
+
+_error_string :: proc(errno: i32) -> string {
+	e := wasi.errno_t(errno)
+	if e == .NONE {
+		return ""
+	}
+
+	err := runtime.Type_Info_Enum_Value(e)
+
+	ti := &runtime.type_info_base(type_info_of(wasi.errno_t)).variant.(runtime.Type_Info_Enum)
+	if idx, ok := slice.binary_search(ti.values, err); ok {
+		return ti.names[idx]
+	}
+	return "<unknown platform error>"
+}
+
+_get_platform_error :: proc(errno: wasi.errno_t) -> Error {
+	#partial switch errno {
+	case .PERM:
+		return .Permission_Denied
+	case .EXIST:
+		return .Exist
+	case .NOENT:
+		return .Not_Exist
+	case .TIMEDOUT:
+		return .Timeout
+	case .PIPE:
+		return .Broken_Pipe
+	case .BADF:
+		return .Invalid_File
+	case .NOMEM:
+		return .Out_Of_Memory
+	case .NOSYS:
+		return .Unsupported
+	case:
+		return Platform_Error(errno)
+	}
+}

+ 534 - 0
core/os/os2/file_wasi.odin

@@ -0,0 +1,534 @@
+#+private
+package os2
+
+import "base:runtime"
+
+import "core:io"
+import "core:sys/wasm/wasi"
+import "core:time"
+
+// NOTE: Don't know if there is a max in wasi.
+MAX_RW :: 1 << 30
+
+File_Impl :: struct {
+	file:      File,
+	name:      string,
+	fd:        wasi.fd_t,
+	allocator: runtime.Allocator,
+}
+
+// WASI works with "preopened" directories, the environment retrieves directories
+// (for example with `wasmtime --dir=. module.wasm`) and those given directories
+// are the only ones accessible by the application.
+//
+// So in order to facilitate the `os` API (absolute paths etc.) we keep a list
+// of the given directories and match them when needed (notably `os.open`).
+Preopen :: struct {
+	fd:     wasi.fd_t,
+	prefix: string,
+}
+preopens: []Preopen
+
+@(init)
+init_std_files :: proc() {
+	new_std :: proc(impl: ^File_Impl, fd: wasi.fd_t, name: string) -> ^File {
+		impl.file.impl = impl
+		impl.allocator = runtime.nil_allocator()
+		impl.fd = fd
+		impl.name  = string(name)
+		impl.file.stream = {
+			data = impl,
+			procedure = _file_stream_proc,
+		}
+		impl.file.fstat = _fstat
+		return &impl.file
+	}
+
+	@(static) files: [3]File_Impl
+	stdin  = new_std(&files[0], 0, "/dev/stdin")
+	stdout = new_std(&files[1], 1, "/dev/stdout")
+	stderr = new_std(&files[2], 2, "/dev/stderr")
+}
+
+@(init)
+init_preopens :: proc() {
+	strip_prefixes :: proc(path: string) -> string {
+		path := path
+		loop: for len(path) > 0 {
+			switch {
+			case path[0] == '/':
+				path = path[1:]
+			case len(path) > 2  && path[0] == '.' && path[1] == '/':
+				path = path[2:]
+			case len(path) == 1 && path[0] == '.':
+				path = path[1:]
+			case:
+				break loop
+			}
+		}
+		return path
+	}
+
+	n: int
+	n_loop: for fd := wasi.fd_t(3); ; fd += 1 {
+		_, err := wasi.fd_prestat_get(fd)
+		#partial switch err {
+		case .BADF:    break n_loop
+		case .SUCCESS: n += 1
+		case:
+			print_error(stderr, _get_platform_error(err), "unexpected error from wasi_prestat_get")
+			break n_loop
+		}
+	}
+
+	alloc_err: runtime.Allocator_Error
+	preopens, alloc_err = make([]Preopen, n, file_allocator())
+	if alloc_err != nil {
+		print_error(stderr, alloc_err, "could not allocate memory for wasi preopens")
+		return
+	}
+
+	loop: for &preopen, i in preopens {
+		fd := wasi.fd_t(3 + i)
+
+		desc, err := wasi.fd_prestat_get(fd)
+		assert(err == .SUCCESS)
+
+		switch desc.tag {
+		case .DIR:
+			buf: []byte
+			buf, alloc_err = make([]byte, desc.dir.pr_name_len, file_allocator())
+			if alloc_err != nil {
+				print_error(stderr, alloc_err, "could not allocate memory for wasi preopen dir name")
+				continue loop
+			}
+
+			if err = wasi.fd_prestat_dir_name(fd, buf); err != .SUCCESS {
+				print_error(stderr, _get_platform_error(err), "could not get filesystem preopen dir name")
+				continue loop
+			}
+
+			preopen.fd = fd
+			preopen.prefix = strip_prefixes(string(buf))
+		}
+	}
+}
+
+@(require_results)
+match_preopen :: proc(path: string) -> (wasi.fd_t, string, bool) {
+	@(require_results)
+	prefix_matches :: proc(prefix, path: string) -> bool {
+		// Empty is valid for any relative path.
+		if len(prefix) == 0 && len(path) > 0 && path[0] != '/' {
+			return true
+		}
+
+		if len(path) < len(prefix) {
+			return false
+		}
+
+		if path[:len(prefix)] != prefix {
+			return false
+		}
+
+		// Only match on full components.
+		i := len(prefix)
+		for i > 0 && prefix[i-1] == '/' {
+			i -= 1
+		}
+		return path[i] == '/'
+	}
+
+	path := path
+	if path == "" {
+		return 0, "", false
+	}
+
+	for len(path) > 0 && path[0] == '/' {
+		path = path[1:]
+	}
+
+	match: Preopen
+	#reverse for preopen in preopens {
+		if (match.fd == 0 || len(preopen.prefix) > len(match.prefix)) && prefix_matches(preopen.prefix, path) {
+			match = preopen
+		}
+	}
+
+	if match.fd == 0 {
+		return 0, "", false
+	}
+
+	relative := path[len(match.prefix):]
+	for len(relative) > 0 && relative[0] == '/' {
+		relative = relative[1:]
+	}
+
+	if len(relative) == 0 {
+		relative = "."
+	}
+
+	return match.fd, relative, true
+}
+
+_open :: proc(name: string, flags: File_Flags, perm: int) -> (f: ^File, err: Error) {
+	dir_fd, relative, ok := match_preopen(name)
+	if !ok {
+		return nil, .Invalid_Path
+	}
+
+	oflags: wasi.oflags_t
+	if .Create in flags { oflags += {.CREATE} }
+	if .Excl   in flags { oflags += {.EXCL} }
+	if .Trunc  in flags { oflags += {.TRUNC} }
+
+	fdflags: wasi.fdflags_t
+	if .Append in flags { fdflags += {.APPEND} }
+	if .Sync   in flags { fdflags += {.SYNC} }
+
+	// NOTE: rights are adjusted to what this package's functions might want to call.
+	rights: wasi.rights_t
+	if .Read  in flags { rights += {.FD_READ, .FD_FILESTAT_GET, .PATH_FILESTAT_GET} }
+	if .Write in flags { rights += {.FD_WRITE, .FD_SYNC, .FD_FILESTAT_SET_SIZE, .FD_FILESTAT_SET_TIMES, .FD_SEEK} }
+
+	fd, fderr := wasi.path_open(dir_fd, {.SYMLINK_FOLLOW}, relative, oflags, rights, {}, fdflags)
+	if fderr != nil {
+		err = _get_platform_error(fderr)
+		return
+	}
+
+	return _new_file(uintptr(fd), name, file_allocator())
+}
+
+_new_file :: proc(handle: uintptr, name: string, allocator: runtime.Allocator) -> (f: ^File, err: Error) {
+	if name == "" {
+		err = .Invalid_Path
+		return
+	}
+
+	impl := new(File_Impl, allocator) or_return
+	defer if err != nil { free(impl, allocator) }
+
+	impl.allocator = allocator
+	// NOTE: wasi doesn't really do full paths afact.
+	impl.name = clone_string(name, allocator) or_return
+	impl.fd = wasi.fd_t(handle)
+	impl.file.impl = impl
+	impl.file.stream = {
+		data = impl,
+		procedure = _file_stream_proc,
+	}
+	impl.file.fstat = _fstat
+
+	return &impl.file, nil
+}
+
+_close :: proc(f: ^File_Impl) -> (err: Error) {
+	if errno := wasi.fd_close(f.fd); errno != nil {
+		err = _get_platform_error(errno)
+	}
+
+	delete(f.name, f.allocator)
+	free(f, f.allocator)
+	return
+}
+
+_fd :: proc(f: ^File) -> uintptr {
+	return uintptr(__fd(f))
+}
+
+__fd :: proc(f: ^File) -> wasi.fd_t {
+	if f != nil && f.impl != nil {
+		return (^File_Impl)(f.impl).fd
+	}
+	return -1
+}
+
+_name :: proc(f: ^File) -> string {
+	if f != nil && f.impl != nil {
+		return (^File_Impl)(f.impl).name
+	}
+	return ""
+}
+
+_sync :: proc(f: ^File) -> Error {
+	return _get_platform_error(wasi.fd_sync(__fd(f)))
+}
+
+_truncate :: proc(f: ^File, size: i64) -> Error {
+	return _get_platform_error(wasi.fd_filestat_set_size(__fd(f), wasi.filesize_t(size)))
+}
+
+_remove :: proc(name: string) -> Error {
+	dir_fd, relative, ok := match_preopen(name)
+	if !ok {
+		return .Invalid_Path
+	}
+
+	err := wasi.path_remove_directory(dir_fd, relative)
+	if err == .NOTDIR {
+		err = wasi.path_unlink_file(dir_fd, relative)
+	}
+
+	return _get_platform_error(err)
+}
+
+_rename :: proc(old_path, new_path: string) -> Error {
+	src_dir_fd, src_relative, src_ok := match_preopen(old_path)
+	if !src_ok {
+		return .Invalid_Path
+	}
+
+	new_dir_fd, new_relative, new_ok := match_preopen(new_path)
+	if !new_ok {
+		return .Invalid_Path
+	}
+
+	return _get_platform_error(wasi.path_rename(src_dir_fd, src_relative, new_dir_fd, new_relative))
+}
+
+_link :: proc(old_name, new_name: string) -> Error {
+	src_dir_fd, src_relative, src_ok := match_preopen(old_name)
+	if !src_ok {
+		return .Invalid_Path
+	}
+
+	new_dir_fd, new_relative, new_ok := match_preopen(new_name)
+	if !new_ok {
+		return .Invalid_Path
+	}
+
+	return _get_platform_error(wasi.path_link(src_dir_fd, {.SYMLINK_FOLLOW}, src_relative, new_dir_fd, new_relative))
+}
+
+_symlink :: proc(old_name, new_name: string) -> Error {
+	src_dir_fd, src_relative, src_ok := match_preopen(old_name)
+	if !src_ok {
+		return .Invalid_Path
+	}
+
+	new_dir_fd, new_relative, new_ok := match_preopen(new_name)
+	if !new_ok {
+		return .Invalid_Path
+	}
+
+	if src_dir_fd != new_dir_fd {
+		return .Invalid_Path
+	}
+
+	return _get_platform_error(wasi.path_symlink(src_relative, src_dir_fd, new_relative))
+}
+
+_read_link :: proc(name: string, allocator: runtime.Allocator) -> (s: string, err: Error) {
+	dir_fd, relative, ok := match_preopen(name)
+	if !ok {
+		return "", .Invalid_Path
+	}
+
+	n, _err := wasi.path_readlink(dir_fd, relative, nil)
+	if _err != nil {
+		err = _get_platform_error(_err)
+		return
+	}
+
+	buf := make([]byte, n, allocator) or_return
+
+	_, _err = wasi.path_readlink(dir_fd, relative, buf)
+	s = string(buf)
+	err = _get_platform_error(_err)
+	return
+}
+
+_chdir :: proc(name: string) -> Error {
+	return .Unsupported
+}
+
+_fchdir :: proc(f: ^File) -> Error {
+	return .Unsupported
+}
+
+_fchmod :: proc(f: ^File, mode: int) -> Error {
+	return .Unsupported
+}
+
+_chmod :: proc(name: string, mode: int) -> Error {
+	return .Unsupported
+}
+
+_fchown :: proc(f: ^File, uid, gid: int) -> Error {
+	return .Unsupported
+}
+
+_chown :: proc(name: string, uid, gid: int) -> Error {
+	return .Unsupported
+}
+
+_lchown :: proc(name: string, uid, gid: int) -> Error {
+	return .Unsupported
+}
+
+_chtimes :: proc(name: string, atime, mtime: time.Time) -> Error {
+	dir_fd, relative, ok := match_preopen(name)
+	if !ok {
+		return .Invalid_Path
+	}
+
+	_atime := wasi.timestamp_t(atime._nsec)
+	_mtime := wasi.timestamp_t(mtime._nsec)
+
+	return _get_platform_error(wasi.path_filestat_set_times(dir_fd, {.SYMLINK_FOLLOW}, relative, _atime, _mtime, {.MTIM, .ATIM}))
+}
+
+_fchtimes :: proc(f: ^File, atime, mtime: time.Time) -> Error {
+	_atime := wasi.timestamp_t(atime._nsec)
+	_mtime := wasi.timestamp_t(mtime._nsec)
+
+	return _get_platform_error(wasi.fd_filestat_set_times(__fd(f), _atime, _mtime, {.ATIM, .MTIM}))
+}
+
+_exists :: proc(path: string) -> bool {
+	dir_fd, relative, ok := match_preopen(path)
+	if !ok {
+		return false
+	}
+
+	_, err := wasi.path_filestat_get(dir_fd, {.SYMLINK_FOLLOW}, relative)
+	if err != nil {
+		return false
+	}
+
+	return true
+}
+
+_file_stream_proc :: proc(stream_data: rawptr, mode: io.Stream_Mode, p: []byte, offset: i64, whence: io.Seek_From) -> (n: i64, err: io.Error) {
+	f  := (^File_Impl)(stream_data)
+	fd := f.fd
+
+	switch mode {
+	case .Read:
+		if len(p) <= 0 {
+			return
+		}
+
+		to_read := min(len(p), MAX_RW)
+		_n, _err := wasi.fd_read(fd, {p[:to_read]})
+		n = i64(_n)
+
+		if _err != nil {
+			err = .Unknown
+		} else if n == 0 {
+			err = .EOF
+		}
+
+		return
+
+	case .Read_At:
+		if len(p) <= 0 {
+			return
+		}
+
+		if offset < 0 {
+			err = .Invalid_Offset
+			return
+		}
+
+		to_read := min(len(p), MAX_RW)
+		_n, _err := wasi.fd_pread(fd, {p[:to_read]}, wasi.filesize_t(offset))
+		n = i64(_n)
+
+		if _err != nil {
+			err = .Unknown
+		} else if n == 0 {
+			err = .EOF
+		}
+
+		return
+
+	case .Write:
+		p := p
+		for len(p) > 0 {
+			to_write := min(len(p), MAX_RW)
+			_n, _err := wasi.fd_write(fd, {p[:to_write]})
+			if _err != nil {
+				err = .Unknown
+				return
+			}
+			p = p[_n:]
+			n += i64(_n)
+		}
+		return
+
+	case .Write_At:
+		p := p
+		offset := offset
+
+		if offset < 0 {
+			err = .Invalid_Offset
+			return
+		}
+
+		for len(p) > 0 {
+			to_write := min(len(p), MAX_RW)
+			_n, _err := wasi.fd_pwrite(fd, {p[:to_write]}, wasi.filesize_t(offset))
+			if _err != nil {
+				err = .Unknown
+				return
+			}
+
+			p = p[_n:]
+			n += i64(_n)
+			offset += i64(_n)
+		}
+		return
+
+	case .Seek:
+		#assert(int(wasi.whence_t.SET) == int(io.Seek_From.Start))
+		#assert(int(wasi.whence_t.CUR) == int(io.Seek_From.Current))
+		#assert(int(wasi.whence_t.END) == int(io.Seek_From.End))
+
+		switch whence {
+		case .Start, .Current, .End:
+			break
+		case:
+			err = .Invalid_Whence
+			return
+		}
+
+		_n, _err := wasi.fd_seek(fd, wasi.filedelta_t(offset), wasi.whence_t(whence))
+		#partial switch _err {
+		case .INVAL:
+			err = .Invalid_Offset
+		case:
+			err = .Unknown
+		case .SUCCESS:
+			n = i64(_n)
+		}
+		return
+
+	case .Size:
+		stat, _err := wasi.fd_filestat_get(fd)
+		if _err != nil {
+			err = .Unknown
+			return
+		}
+
+		n = i64(stat.size)
+		return
+
+	case .Flush:
+		ferr := _sync(&f.file)
+		err   = error_to_io_error(ferr)
+		return
+
+	case .Close, .Destroy:
+		ferr := _close(f)
+		err   = error_to_io_error(ferr)
+		return
+
+	case .Query:
+		return io.query_utility({.Read, .Read_At, .Write, .Write_At, .Seek, .Size, .Flush, .Close, .Destroy, .Query})
+
+	case:
+		return 0, .Empty
+	}
+}

+ 6 - 0
core/os/os2/heap_wasi.odin

@@ -0,0 +1,6 @@
+#+private
+package os2
+
+import "base:runtime"
+
+_heap_allocator_proc :: runtime.wasm_allocator_proc

+ 1 - 1
core/os/os2/path_posix.odin

@@ -81,7 +81,7 @@ _remove_all :: proc(path: string) -> Error {
 
 		fullpath, _ := concatenate({path, "/", string(cname), "\x00"}, temp_allocator())
 		if entry.d_type == .DIR {
-			_remove_all(fullpath[:len(fullpath)-1])
+			_remove_all(fullpath[:len(fullpath)-1]) or_return
 		} else {
 			if posix.unlink(cstring(raw_data(fullpath))) != .OK {
 				return _get_platform_error()

+ 84 - 0
core/os/os2/path_wasi.odin

@@ -0,0 +1,84 @@
+#+private
+package os2
+
+import "base:runtime"
+
+import "core:path/filepath"
+import "core:sys/wasm/wasi"
+
+_Path_Separator        :: '/'
+_Path_Separator_String :: "/"
+_Path_List_Separator   :: ':'
+
+_is_path_separator :: proc(c: byte) -> bool {
+	return c == _Path_Separator
+}
+
+_mkdir :: proc(name: string, perm: int) -> Error {
+	dir_fd, relative, ok := match_preopen(name)
+	if !ok {
+		return .Invalid_Path
+	}
+
+	return _get_platform_error(wasi.path_create_directory(dir_fd, relative))
+}
+
+_mkdir_all :: proc(path: string, perm: int) -> Error {
+	if path == "" {
+		return .Invalid_Path
+	}
+
+	TEMP_ALLOCATOR_GUARD()
+
+	if exists(path) {
+		return .Exist
+	}
+
+	clean_path := filepath.clean(path, temp_allocator())
+	return internal_mkdir_all(clean_path)
+
+	internal_mkdir_all :: proc(path: string) -> Error {
+		dir, file := filepath.split(path)
+		if file != path && dir != "/" {
+			if len(dir) > 1 && dir[len(dir) - 1] == '/' {
+				dir = dir[:len(dir) - 1]
+			}
+			internal_mkdir_all(dir) or_return
+		}
+
+		err := _mkdir(path, 0)
+		if err == .Exist { err = nil }
+		return err
+	}
+}
+
+_remove_all :: proc(path: string) -> (err: Error) {
+	//  PERF: this works, but wastes a bunch of memory using the read_directory_iterator API
+	// and using open instead of wasi fds directly.
+	{
+		dir := open(path) or_return
+		defer close(dir)
+
+		iter := read_directory_iterator_create(dir) or_return
+		defer read_directory_iterator_destroy(&iter)
+
+		for fi in read_directory_iterator(&iter) {
+			if fi.type == .Directory {
+				_remove_all(fi.fullpath) or_return
+			} else {
+				remove(fi.fullpath) or_return
+			}
+		}
+	}
+
+	return remove(path)
+}
+
+_get_working_directory :: proc(allocator: runtime.Allocator) -> (dir: string, err: Error) {
+	return ".", .Unsupported
+}
+
+_set_working_directory :: proc(dir: string) -> (err: Error) {
+	err = .Unsupported
+	return
+}

+ 13 - 0
core/os/os2/pipe_wasi.odin

@@ -0,0 +1,13 @@
+#+private
+package os2
+
+_pipe :: proc() -> (r, w: ^File, err: Error) {
+	err = .Unsupported
+	return
+}
+
+@(require_results)
+_pipe_has_data :: proc(r: ^File) -> (ok: bool, err: Error) {
+	err = .Unsupported
+	return
+}

+ 89 - 0
core/os/os2/process_wasi.odin

@@ -0,0 +1,89 @@
+#+private
+package os2
+
+import "base:runtime"
+
+import "core:time"
+import "core:sys/wasm/wasi"
+
+_exit :: proc "contextless" (code: int) -> ! {
+	wasi.proc_exit(wasi.exitcode_t(code))
+}
+
+_get_uid :: proc() -> int {
+	return 0
+}
+
+_get_euid :: proc() -> int {
+	return 0
+}
+
+_get_gid :: proc() -> int {
+	return 0
+}
+
+_get_egid :: proc() -> int {
+	return 0
+}
+
+_get_pid :: proc() -> int {
+	return 0
+}
+
+_get_ppid :: proc() -> int {
+	return 0
+}
+
+_process_info_by_handle :: proc(process: Process, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) {
+	err = .Unsupported
+	return
+}
+
+_current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) {
+	err = .Unsupported
+	return
+}
+
+_Sys_Process_Attributes :: struct {}
+
+_process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) {
+	err = .Unsupported
+	return
+}
+
+_process_wait :: proc(process: Process, timeout: time.Duration) -> (process_state: Process_State, err: Error) {
+	err = .Unsupported
+	return
+}
+
+_process_close :: proc(process: Process) -> Error {
+	return .Unsupported
+}
+
+_process_kill :: proc(process: Process) -> (err: Error) {
+	return .Unsupported
+}
+
+_process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) {
+	err = .Unsupported
+	return
+}
+
+_process_list :: proc(allocator: runtime.Allocator) -> (list: []int, err: Error) {
+	err = .Unsupported
+	return
+}
+
+_process_open :: proc(pid: int, flags: Process_Open_Flags) -> (process: Process, err: Error) {
+	process.pid = pid
+	err = .Unsupported
+	return
+}
+
+_process_handle_still_valid :: proc(p: Process) -> Error {
+	return nil
+}
+
+_process_state_update_times :: proc(p: Process, state: ^Process_State) {
+	return
+}

+ 101 - 0
core/os/os2/stat_wasi.odin

@@ -0,0 +1,101 @@
+#+private
+package os2
+
+import "base:runtime"
+
+import "core:path/filepath"
+import "core:sys/wasm/wasi"
+import "core:time"
+
+internal_stat :: proc(stat: wasi.filestat_t, fullpath: string) -> (fi: File_Info) {
+	fi.fullpath = fullpath
+	fi.name = filepath.base(fi.fullpath)
+
+	fi.inode = u128(stat.ino)
+	fi.size  = i64(stat.size)
+
+	switch stat.filetype {
+	case .BLOCK_DEVICE:                 fi.type = .Block_Device
+	case .CHARACTER_DEVICE:             fi.type = .Character_Device
+	case .DIRECTORY:                    fi.type = .Directory
+	case .REGULAR_FILE:                 fi.type = .Regular
+	case .SOCKET_DGRAM, .SOCKET_STREAM: fi.type = .Socket
+	case .SYMBOLIC_LINK:                fi.type = .Symlink
+	case .UNKNOWN:                      fi.type = .Undetermined
+	case:                               fi.type = .Undetermined
+	}
+
+	fi.creation_time     = time.Time{_nsec=i64(stat.ctim)}
+	fi.modification_time = time.Time{_nsec=i64(stat.mtim)}
+	fi.access_time       = time.Time{_nsec=i64(stat.atim)}
+
+	return
+}
+
+_fstat :: proc(f: ^File, allocator: runtime.Allocator) -> (fi: File_Info, err: Error) {
+	if f == nil || f.impl == nil {
+		err = .Invalid_File
+		return
+	}
+
+	impl := (^File_Impl)(f.impl)
+
+	stat, _err := wasi.fd_filestat_get(__fd(f))
+	if _err != nil {
+		err = _get_platform_error(_err)
+		return
+	}
+
+	fullpath := clone_string(impl.name, allocator) or_return
+	return internal_stat(stat, fullpath), nil
+}
+
+_stat :: proc(name: string, allocator: runtime.Allocator) -> (fi: File_Info, err: Error) {
+	if name == "" {
+		err = .Invalid_Path
+		return
+	}
+
+	dir_fd, relative, ok := match_preopen(name)
+	if !ok {
+		err = .Invalid_Path
+		return
+	}
+
+	stat, _err := wasi.path_filestat_get(dir_fd, {.SYMLINK_FOLLOW}, relative)
+	if _err != nil {
+		err = _get_platform_error(_err)
+		return
+	}
+
+	// NOTE: wasi doesn't really do full paths afact.
+	fullpath := clone_string(name, allocator) or_return
+	return internal_stat(stat, fullpath), nil
+}
+
+_lstat :: proc(name: string, allocator: runtime.Allocator) -> (fi: File_Info, err: Error) {
+	if name == "" {
+		err = .Invalid_Path
+		return
+	}
+
+	dir_fd, relative, ok := match_preopen(name)
+	if !ok {
+		err = .Invalid_Path
+		return
+	}
+
+	stat, _err := wasi.path_filestat_get(dir_fd, {}, relative)
+	if _err != nil {
+		err = _get_platform_error(_err)
+		return
+	}
+
+	// NOTE: wasi doesn't really do full paths afact.
+	fullpath := clone_string(name, allocator) or_return
+	return internal_stat(stat, fullpath), nil
+}
+
+_same_file :: proc(fi1, fi2: File_Info) -> bool {
+	return fi1.fullpath == fi2.fullpath
+}

+ 9 - 0
core/os/os2/temp_file_wasi.odin

@@ -0,0 +1,9 @@
+#+private
+package os2
+
+import "base:runtime"
+
+_temp_dir :: proc(allocator: runtime.Allocator) -> (string, runtime.Allocator_Error) {
+	// NOTE: requires user to add /tmp to their preopen dirs, no standard way exists.
+	return clone_string("/tmp", allocator)
+}

+ 1 - 0
core/path/filepath/match.odin

@@ -1,3 +1,4 @@
+#+build !wasi
 package filepath
 
 import "core:os"

+ 36 - 0
core/path/filepath/path_wasi.odin

@@ -0,0 +1,36 @@
+package filepath
+
+import "base:runtime"
+
+import "core:strings"
+
+SEPARATOR :: '/'
+SEPARATOR_STRING :: `/`
+LIST_SEPARATOR :: ':'
+
+is_reserved_name :: proc(path: string) -> bool {
+	return false
+}
+
+is_abs :: proc(path: string) -> bool {
+	return strings.has_prefix(path, "/")
+}
+
+abs :: proc(path: string, allocator := context.allocator) -> (string, bool) {
+	if is_abs(path) {
+		return strings.clone(string(path), allocator), true
+	}
+
+	return path, false
+}
+
+join :: proc(elems: []string, allocator := context.allocator) -> (joined: string, err: runtime.Allocator_Error) #optional_allocator_error {
+	for e, i in elems {
+		if e != "" {
+			runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD(ignore = context.temp_allocator == allocator)
+			p := strings.join(elems[i:], SEPARATOR_STRING, context.temp_allocator) or_return
+			return clean(p, allocator)
+		}
+	}
+	return "", nil
+}

+ 1 - 0
core/path/filepath/walk.odin

@@ -1,3 +1,4 @@
+#+build !wasi
 package filepath
 
 import "core:os"