123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560 |
- #+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
- }
- _clone :: proc(f: ^File) -> (clone: ^File, err: Error) {
- if f == nil || f.impl == nil {
- return
- }
- dir_fd, relative, ok := match_preopen(name(f))
- if !ok {
- return nil, .Invalid_Path
- }
- fd, fderr := wasi.path_open(dir_fd, {.SYMLINK_FOLLOW}, relative, {}, {}, {}, {})
- if fderr != nil {
- err = _get_platform_error(fderr)
- return
- }
- defer if err != nil { wasi.fd_close(fd) }
- fderr = wasi.fd_renumber((^File_Impl)(f.impl).fd, fd)
- if fderr != nil {
- err = _get_platform_error(fderr)
- return
- }
- return _new_file(uintptr(fd), name(f), file_allocator())
- }
- _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
- }
- }
|