wa-lang.org/wazero@v1.0.2/internal/gojs/fs.go (about)

     1  package gojs
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"io/fs"
     8  	"os"
     9  	"syscall"
    10  
    11  	"wa-lang.org/wazero/api"
    12  	"wa-lang.org/wazero/internal/wasm"
    13  )
    14  
    15  var (
    16  	// jsfs = js.Global().Get("fs") // fs_js.go init
    17  	//
    18  	// js.fsCall conventions:
    19  	// * funcWrapper callback is the last parameter
    20  	//   * arg0 is error and up to one result in arg1
    21  	jsfs = newJsVal(refJsfs, "fs").
    22  		addProperties(map[string]interface{}{
    23  			"constants": jsfsConstants, // = jsfs.Get("constants") // init
    24  		}).
    25  		addFunction("open", &jsfsOpen{}).
    26  		addFunction("stat", &jsfsStat{}).
    27  		addFunction("fstat", &jsfsFstat{}).
    28  		addFunction("lstat", &jsfsStat{}). // because fs.FS doesn't support symlink
    29  		addFunction("close", &jsfsClose{}).
    30  		addFunction("read", &jsfsRead{}).
    31  		addFunction("write", &jsfsWrite{}).
    32  		addFunction("readdir", &jsfsReaddir{})
    33  
    34  	// TODO: stub all these with syscall.ENOSYS
    35  	//	* _, err := fsCall("mkdir", path, perm) // syscall.Mkdir
    36  	//	* _, err := fsCall("unlink", path) // syscall.Unlink
    37  	//	* _, err := fsCall("rmdir", path) // syscall.Rmdir
    38  	//	* _, err := fsCall("chmod", path, mode) // syscall.Chmod
    39  	//	* _, err := fsCall("fchmod", fd, mode) // syscall.Fchmod
    40  	//	* _, err := fsCall("chown", path, uint32(uid), uint32(gid)) // syscall.Chown
    41  	//	* _, err := fsCall("fchown", fd, uint32(uid), uint32(gid)) // syscall.Fchown
    42  	//	* _, err := fsCall("lchown", path, uint32(uid), uint32(gid)) // syscall.Lchown
    43  	//	* _, err := fsCall("utimes", path, atime, mtime) // syscall.UtimesNano
    44  	//	* _, err := fsCall("rename", from, to) // syscall.Rename
    45  	//	* _, err := fsCall("truncate", path, length) // syscall.Truncate
    46  	//	* _, err := fsCall("ftruncate", fd, length) // syscall.Ftruncate
    47  	//	* dst, err := fsCall("readlink", path) // syscall.Readlink
    48  	//	* _, err := fsCall("link", path, link) // syscall.Link
    49  	//	* _, err := fsCall("symlink", path, link) // syscall.Symlink
    50  	//	* _, err := fsCall("fsync", fd) // syscall.Fsync
    51  
    52  	// jsfsConstants = jsfs Get("constants") // fs_js.go init
    53  	jsfsConstants = newJsVal(refJsfsConstants, "constants").
    54  			addProperties(map[string]interface{}{
    55  			"O_WRONLY": oWRONLY,
    56  			"O_RDWR":   oRDWR,
    57  			"O_CREAT":  oCREAT,
    58  			"O_TRUNC":  oTRUNC,
    59  			"O_APPEND": oAPPEND,
    60  			"O_EXCL":   oEXCL,
    61  		})
    62  
    63  	// oWRONLY = jsfsConstants Get("O_WRONLY").Int() // fs_js.go init
    64  	oWRONLY = api.EncodeF64(float64(os.O_WRONLY))
    65  
    66  	// oRDWR = jsfsConstants Get("O_RDWR").Int() // fs_js.go init
    67  	oRDWR = api.EncodeF64(float64(os.O_RDWR))
    68  
    69  	// o CREAT = jsfsConstants Get("O_CREAT").Int() // fs_js.go init
    70  	oCREAT = api.EncodeF64(float64(os.O_CREATE))
    71  
    72  	// oTRUNC = jsfsConstants Get("O_TRUNC").Int() // fs_js.go init
    73  	oTRUNC = api.EncodeF64(float64(os.O_TRUNC))
    74  
    75  	// oAPPEND = jsfsConstants Get("O_APPEND").Int() // fs_js.go init
    76  	oAPPEND = api.EncodeF64(float64(os.O_APPEND))
    77  
    78  	// oEXCL = jsfsConstants Get("O_EXCL").Int() // fs_js.go init
    79  	oEXCL = api.EncodeF64(float64(os.O_EXCL))
    80  )
    81  
    82  // jsfsOpen implements fs.Open
    83  //
    84  //	jsFD /* Int */, err := fsCall("open", path, flags, perm)
    85  type jsfsOpen struct{}
    86  
    87  // invoke implements jsFn.invoke
    88  func (*jsfsOpen) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
    89  	name := args[0].(string)
    90  	flags := toUint32(args[1]) // flags are derived from constants like oWRONLY
    91  	perm := toUint32(args[2])
    92  	callback := args[3].(funcWrapper)
    93  
    94  	fd, err := syscallOpen(ctx, mod, name, flags, perm)
    95  	return callback.invoke(ctx, mod, refJsfs, err, fd) // note: error first
    96  }
    97  
    98  // jsfsStat is used for syscall.Stat
    99  //
   100  //	jsSt, err := fsCall("stat", path)
   101  type jsfsStat struct{}
   102  
   103  // invoke implements jsFn.invoke
   104  func (*jsfsStat) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
   105  	name := args[0].(string)
   106  	callback := args[1].(funcWrapper)
   107  
   108  	stat, err := syscallStat(ctx, mod, name)
   109  	return callback.invoke(ctx, mod, refJsfs, err, stat) // note: error first
   110  }
   111  
   112  // syscallStat is like syscall.Stat
   113  func syscallStat(ctx context.Context, mod api.Module, name string) (*jsSt, error) {
   114  	fsc := mod.(*wasm.CallContext).Sys.FS(ctx)
   115  	if fd, err := fsc.OpenFile(ctx, name); err != nil {
   116  		return nil, err
   117  	} else {
   118  		defer fsc.CloseFile(ctx, fd)
   119  		return syscallFstat(ctx, mod, fd)
   120  	}
   121  }
   122  
   123  // jsfsStat is used for syscall.Open
   124  //
   125  //	stat, err := fsCall("fstat", fd); err == nil && stat.Call("isDirectory").Bool()
   126  type jsfsFstat struct{}
   127  
   128  // invoke implements jsFn.invoke
   129  func (*jsfsFstat) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
   130  	fd := toUint32(args[0])
   131  	callback := args[1].(funcWrapper)
   132  
   133  	fstat, err := syscallFstat(ctx, mod, fd)
   134  	return callback.invoke(ctx, mod, refJsfs, err, fstat) // note: error first
   135  }
   136  
   137  // syscallFstat is like syscall.Fstat
   138  func syscallFstat(ctx context.Context, mod api.Module, fd uint32) (*jsSt, error) {
   139  	fsc := mod.(*wasm.CallContext).Sys.FS(ctx)
   140  	if f, ok := fsc.OpenedFile(ctx, fd); !ok {
   141  		return nil, syscall.EBADF
   142  	} else if stat, err := f.File.Stat(); err != nil {
   143  		return nil, err
   144  	} else {
   145  		ret := &jsSt{}
   146  		ret.isDir = stat.IsDir()
   147  		// TODO ret.dev=stat.Sys
   148  		ret.mode = uint32(stat.Mode())
   149  		ret.size = uint32(stat.Size())
   150  		ret.mtimeMs = uint32(stat.ModTime().UnixMilli())
   151  		return ret, nil
   152  	}
   153  }
   154  
   155  // jsfsClose is used for syscall.Close
   156  //
   157  //	_, err := fsCall("close", fd)
   158  type jsfsClose struct{}
   159  
   160  // invoke implements jsFn.invoke
   161  func (*jsfsClose) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
   162  	fd := toUint32(args[0])
   163  	callback := args[1].(funcWrapper)
   164  
   165  	err := syscallClose(ctx, mod, fd)
   166  	return callback.invoke(ctx, mod, refJsfs, err, true) // note: error first
   167  }
   168  
   169  // syscallClose is like syscall.Close
   170  func syscallClose(ctx context.Context, mod api.Module, fd uint32) (err error) {
   171  	fsc := mod.(*wasm.CallContext).Sys.FS(ctx)
   172  	if ok := fsc.CloseFile(ctx, fd); !ok {
   173  		err = syscall.EBADF // already closed
   174  	}
   175  	return
   176  }
   177  
   178  // jsfsRead is used in syscall.Read and syscall.Pread, called by
   179  // src/internal/poll/fd_unix.go poll.Read.
   180  //
   181  //	n, err := fsCall("read", fd, buf, 0, len(b), nil)
   182  type jsfsRead struct{}
   183  
   184  // invoke implements jsFn.invoke
   185  func (*jsfsRead) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
   186  	fd := toUint32(args[0])
   187  	buf, ok := args[1].(*byteArray)
   188  	if !ok {
   189  		return nil, fmt.Errorf("arg[1] is %v not a []byte", args[1])
   190  	}
   191  	offset := toUint32(args[2])
   192  	byteCount := toUint32(args[3])
   193  	fOffset := args[4] // nil unless Pread
   194  	callback := args[5].(funcWrapper)
   195  
   196  	n, err := syscallRead(ctx, mod, fd, fOffset, buf.slice[offset:offset+byteCount])
   197  	return callback.invoke(ctx, mod, refJsfs, err, n) // note: error first
   198  }
   199  
   200  // syscallRead is like syscall.Read
   201  func syscallRead(ctx context.Context, mod api.Module, fd uint32, offset interface{}, p []byte) (n uint32, err error) {
   202  	r := fdReader(ctx, mod, fd)
   203  	if r == nil {
   204  		err = syscall.EBADF
   205  	}
   206  
   207  	if offset != nil {
   208  		if s, ok := r.(io.Seeker); ok {
   209  			if _, err := s.Seek(toInt64(offset), io.SeekStart); err != nil {
   210  				return 0, err
   211  			}
   212  		} else {
   213  			return 0, syscall.ENOTSUP
   214  		}
   215  	}
   216  
   217  	if nRead, e := r.Read(p); e == nil || e == io.EOF {
   218  		// fs_js.go cannot parse io.EOF so coerce it to nil.
   219  		// See https://github.com/golang/go/issues/43913
   220  		n = uint32(nRead)
   221  	} else {
   222  		err = e
   223  	}
   224  	return
   225  }
   226  
   227  // jsfsWrite is used in syscall.Write and syscall.Pwrite.
   228  //
   229  // Notably, offset is non-nil in Pwrite.
   230  //
   231  //	n, err := fsCall("write", fd, buf, 0, len(b), nil)
   232  type jsfsWrite struct{}
   233  
   234  // invoke implements jsFn.invoke
   235  func (*jsfsWrite) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
   236  	fd := toUint32(args[0])
   237  	buf, ok := args[1].(*byteArray)
   238  	if !ok {
   239  		return nil, fmt.Errorf("arg[1] is %v not a []byte", args[1])
   240  	}
   241  	offset := toUint32(args[2])
   242  	byteCount := toUint32(args[3])
   243  	fOffset := args[4] // nil unless Pread
   244  	callback := args[5].(funcWrapper)
   245  
   246  	if byteCount > 0 { // empty is possible on EOF
   247  		n, err := syscallWrite(ctx, mod, fd, fOffset, buf.slice[offset:offset+byteCount])
   248  		return callback.invoke(ctx, mod, refJsfs, err, n) // note: error first
   249  	}
   250  	return callback.invoke(ctx, mod, refJsfs, nil, refValueZero)
   251  }
   252  
   253  // syscallWrite is like syscall.Write
   254  func syscallWrite(ctx context.Context, mod api.Module, fd uint32, offset interface{}, p []byte) (n uint32, err error) {
   255  	if writer := fdWriter(ctx, mod, fd); writer == nil {
   256  		err = syscall.EBADF
   257  	} else if nWritten, e := writer.Write(p); e == nil || e == io.EOF {
   258  		// fs_js.go cannot parse io.EOF so coerce it to nil.
   259  		// See https://github.com/golang/go/issues/43913
   260  		n = uint32(nWritten)
   261  	} else {
   262  		err = e
   263  	}
   264  	return
   265  }
   266  
   267  // jsfsReaddir is used in syscall.Open
   268  //
   269  //	dir, err := fsCall("readdir", path)
   270  //		dir.Length(), dir.Index(i).String()
   271  type jsfsReaddir struct{}
   272  
   273  // invoke implements jsFn.invoke
   274  func (*jsfsReaddir) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
   275  	name := args[0].(string)
   276  	callback := args[1].(funcWrapper)
   277  
   278  	stat, err := syscallReaddir(ctx, mod, name)
   279  	return callback.invoke(ctx, mod, refJsfs, err, stat) // note: error first
   280  }
   281  
   282  func syscallReaddir(ctx context.Context, mod api.Module, name string) (*objectArray, error) {
   283  	fsc := mod.(*wasm.CallContext).Sys.FS(ctx)
   284  	fd, err := fsc.OpenFile(ctx, name)
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  	defer fsc.CloseFile(ctx, fd)
   289  
   290  	if f, ok := fsc.OpenedFile(ctx, fd); !ok {
   291  		return nil, syscall.EBADF
   292  	} else if d, ok := f.File.(fs.ReadDirFile); !ok {
   293  		return nil, syscall.ENOTDIR
   294  	} else if l, err := d.ReadDir(-1); err != nil {
   295  		return nil, err
   296  	} else {
   297  		entries := make([]interface{}, 0, len(l))
   298  		for _, e := range l {
   299  			entries = append(entries, e.Name())
   300  		}
   301  		return &objectArray{entries}, nil
   302  	}
   303  }
   304  
   305  type returnZero struct{}
   306  
   307  // invoke implements jsFn.invoke
   308  func (*returnZero) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
   309  	return refValueZero, nil
   310  }
   311  
   312  type returnSliceOfZero struct{}
   313  
   314  // invoke implements jsFn.invoke
   315  func (*returnSliceOfZero) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
   316  	return &objectArray{slice: []interface{}{refValueZero}}, nil
   317  }
   318  
   319  type returnArg0 struct{}
   320  
   321  // invoke implements jsFn.invoke
   322  func (*returnArg0) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
   323  	return args[0], nil
   324  }
   325  
   326  // cwd for fs.Open syscall.Getcwd in fs_js.go
   327  type cwd struct{}
   328  
   329  // invoke implements jsFn.invoke
   330  func (*cwd) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
   331  	return getState(ctx).cwd, nil
   332  }
   333  
   334  // chdir for fs.Open syscall.Chdir in fs_js.go
   335  type chdir struct{}
   336  
   337  // invoke implements jsFn.invoke
   338  func (*chdir) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
   339  	path := args[0].(string)
   340  
   341  	// TODO: refactor so that sys has path-based ops, also needed in WASI.
   342  	fsc := mod.(*wasm.CallContext).Sys.FS(ctx)
   343  	if fd, err := fsc.OpenFile(ctx, path); err != nil {
   344  		return nil, syscall.ENOENT
   345  	} else if f, ok := fsc.OpenedFile(ctx, fd); !ok {
   346  		return nil, syscall.ENOENT
   347  	} else if s, err := f.File.Stat(); err != nil {
   348  		fsc.CloseFile(ctx, fd)
   349  		return nil, syscall.ENOENT
   350  	} else if !s.IsDir() {
   351  		fsc.CloseFile(ctx, fd)
   352  		return nil, syscall.ENOTDIR
   353  	} else {
   354  		getState(ctx).cwd = path
   355  		return nil, nil
   356  	}
   357  }
   358  
   359  // jsSt is pre-parsed from fs_js.go setStat to avoid thrashing
   360  type jsSt struct {
   361  	isDir bool
   362  
   363  	dev     uint32
   364  	ino     uint32
   365  	mode    uint32
   366  	nlink   uint32
   367  	uid     uint32
   368  	gid     uint32
   369  	rdev    uint32
   370  	size    uint32
   371  	blksize uint32
   372  	blocks  uint32
   373  	atimeMs uint32
   374  	mtimeMs uint32
   375  	ctimeMs uint32
   376  }
   377  
   378  // get implements jsGet.get
   379  func (s *jsSt) get(_ context.Context, propertyKey string) interface{} {
   380  	switch propertyKey {
   381  	case "dev":
   382  		return s.dev
   383  	case "ino":
   384  		return s.ino
   385  	case "mode":
   386  		return s.mode
   387  	case "nlink":
   388  		return s.nlink
   389  	case "uid":
   390  		return s.uid
   391  	case "gid":
   392  		return s.gid
   393  	case "rdev":
   394  		return s.rdev
   395  	case "size":
   396  		return s.size
   397  	case "blksize":
   398  		return s.blksize
   399  	case "blocks":
   400  		return s.blocks
   401  	case "atimeMs":
   402  		return s.atimeMs
   403  	case "mtimeMs":
   404  		return s.mtimeMs
   405  	case "ctimeMs":
   406  		return s.ctimeMs
   407  	}
   408  	panic(fmt.Sprintf("TODO: stat.%s", propertyKey))
   409  }
   410  
   411  // call implements jsCall.call
   412  func (s *jsSt) call(ctx context.Context, mod api.Module, this ref, method string, args ...interface{}) (interface{}, error) {
   413  	if method == "isDirectory" {
   414  		return s.isDir, nil
   415  	}
   416  	panic(fmt.Sprintf("TODO: stat.%s", method))
   417  }