| // Copyright 2018 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| //go:build js && wasm | |
| package syscall | |
| import ( | |
| "errors" | |
| "sync" | |
| "syscall/js" | |
| ) | |
| // Provided by package runtime. | |
| func now() (sec int64, nsec int32) | |
| var jsProcess = js.Global().Get("process") | |
| var jsPath = js.Global().Get("path") | |
| var jsFS = js.Global().Get("fs") | |
| var constants = jsFS.Get("constants") | |
| var uint8Array = js.Global().Get("Uint8Array") | |
| var ( | |
| nodeWRONLY = constants.Get("O_WRONLY").Int() | |
| nodeRDWR = constants.Get("O_RDWR").Int() | |
| nodeCREATE = constants.Get("O_CREAT").Int() | |
| nodeTRUNC = constants.Get("O_TRUNC").Int() | |
| nodeAPPEND = constants.Get("O_APPEND").Int() | |
| nodeEXCL = constants.Get("O_EXCL").Int() | |
| // NodeJS on Windows does not support O_DIRECTORY, so we default | |
| // to -1 and assign it in init if available. | |
| // See https://nodejs.org/docs/latest/api/fs.html#file-open-constants. | |
| nodeDIRECTORY = -1 | |
| ) | |
| func init() { | |
| oDir := constants.Get("O_DIRECTORY") | |
| if !oDir.IsUndefined() { | |
| nodeDIRECTORY = oDir.Int() | |
| } | |
| } | |
| type jsFile struct { | |
| path string | |
| entries []string | |
| dirIdx int // entries[:dirIdx] have already been returned in ReadDirent | |
| pos int64 | |
| seeked bool | |
| } | |
| var filesMu sync.Mutex | |
| var files = map[int]*jsFile{ | |
| 0: {}, | |
| 1: {}, | |
| 2: {}, | |
| } | |
| func fdToFile(fd int) (*jsFile, error) { | |
| filesMu.Lock() | |
| f, ok := files[fd] | |
| filesMu.Unlock() | |
| if !ok { | |
| return nil, EBADF | |
| } | |
| return f, nil | |
| } | |
| func Open(path string, openmode int, perm uint32) (int, error) { | |
| if err := checkPath(path); err != nil { | |
| return 0, err | |
| } | |
| flags := 0 | |
| if openmode&O_WRONLY != 0 { | |
| flags |= nodeWRONLY | |
| } | |
| if openmode&O_RDWR != 0 { | |
| flags |= nodeRDWR | |
| } | |
| if openmode&O_CREATE != 0 { | |
| flags |= nodeCREATE | |
| } | |
| if openmode&O_TRUNC != 0 { | |
| flags |= nodeTRUNC | |
| } | |
| if openmode&O_APPEND != 0 { | |
| flags |= nodeAPPEND | |
| } | |
| if openmode&O_EXCL != 0 { | |
| flags |= nodeEXCL | |
| } | |
| if openmode&O_SYNC != 0 { | |
| return 0, errors.New("syscall.Open: O_SYNC is not supported by js/wasm") | |
| } | |
| if openmode&O_DIRECTORY != 0 { | |
| if nodeDIRECTORY != -1 { | |
| flags |= nodeDIRECTORY | |
| } else { | |
| return 0, errors.New("syscall.Open: O_DIRECTORY is not supported on Windows") | |
| } | |
| } | |
| jsFD, err := fsCall("open", path, flags, perm) | |
| if err != nil { | |
| return 0, err | |
| } | |
| fd := jsFD.Int() | |
| var entries []string | |
| if stat, err := fsCall("fstat", fd); err == nil && stat.Call("isDirectory").Bool() { | |
| dir, err := fsCall("readdir", path) | |
| if err != nil { | |
| return 0, err | |
| } | |
| entries = make([]string, dir.Length()) | |
| for i := range entries { | |
| entries[i] = dir.Index(i).String() | |
| } | |
| } | |
| path = jsPath.Call("resolve", path).String() | |
| f := &jsFile{ | |
| path: path, | |
| entries: entries, | |
| } | |
| filesMu.Lock() | |
| files[fd] = f | |
| filesMu.Unlock() | |
| return fd, nil | |
| } | |
| func Close(fd int) error { | |
| filesMu.Lock() | |
| delete(files, fd) | |
| filesMu.Unlock() | |
| _, err := fsCall("close", fd) | |
| return err | |
| } | |
| func CloseOnExec(fd int) { | |
| // nothing to do - no exec | |
| } | |
| func Mkdir(path string, perm uint32) error { | |
| if err := checkPath(path); err != nil { | |
| return err | |
| } | |
| _, err := fsCall("mkdir", path, perm) | |
| return err | |
| } | |
| func ReadDirent(fd int, buf []byte) (int, error) { | |
| f, err := fdToFile(fd) | |
| if err != nil { | |
| return 0, err | |
| } | |
| if f.entries == nil { | |
| return 0, EINVAL | |
| } | |
| n := 0 | |
| for f.dirIdx < len(f.entries) { | |
| entry := f.entries[f.dirIdx] | |
| l := 2 + len(entry) | |
| if l > len(buf) { | |
| break | |
| } | |
| buf[0] = byte(l) | |
| buf[1] = byte(l >> 8) | |
| copy(buf[2:], entry) | |
| buf = buf[l:] | |
| n += l | |
| f.dirIdx++ | |
| } | |
| return n, nil | |
| } | |
| func setStat(st *Stat_t, jsSt js.Value) { | |
| st.Dev = int64(jsSt.Get("dev").Int()) | |
| st.Ino = uint64(jsSt.Get("ino").Int()) | |
| st.Mode = uint32(jsSt.Get("mode").Int()) | |
| st.Nlink = uint32(jsSt.Get("nlink").Int()) | |
| st.Uid = uint32(jsSt.Get("uid").Int()) | |
| st.Gid = uint32(jsSt.Get("gid").Int()) | |
| st.Rdev = int64(jsSt.Get("rdev").Int()) | |
| st.Size = int64(jsSt.Get("size").Int()) | |
| st.Blksize = int32(jsSt.Get("blksize").Int()) | |
| st.Blocks = int32(jsSt.Get("blocks").Int()) | |
| atime := int64(jsSt.Get("atimeMs").Int()) | |
| st.Atime = atime / 1000 | |
| st.AtimeNsec = (atime % 1000) * 1000000 | |
| mtime := int64(jsSt.Get("mtimeMs").Int()) | |
| st.Mtime = mtime / 1000 | |
| st.MtimeNsec = (mtime % 1000) * 1000000 | |
| ctime := int64(jsSt.Get("ctimeMs").Int()) | |
| st.Ctime = ctime / 1000 | |
| st.CtimeNsec = (ctime % 1000) * 1000000 | |
| } | |
| func Stat(path string, st *Stat_t) error { | |
| if err := checkPath(path); err != nil { | |
| return err | |
| } | |
| jsSt, err := fsCall("stat", path) | |
| if err != nil { | |
| return err | |
| } | |
| setStat(st, jsSt) | |
| return nil | |
| } | |
| func Lstat(path string, st *Stat_t) error { | |
| if err := checkPath(path); err != nil { | |
| return err | |
| } | |
| jsSt, err := fsCall("lstat", path) | |
| if err != nil { | |
| return err | |
| } | |
| setStat(st, jsSt) | |
| return nil | |
| } | |
| func Fstat(fd int, st *Stat_t) error { | |
| jsSt, err := fsCall("fstat", fd) | |
| if err != nil { | |
| return err | |
| } | |
| setStat(st, jsSt) | |
| return nil | |
| } | |
| func Unlink(path string) error { | |
| if err := checkPath(path); err != nil { | |
| return err | |
| } | |
| _, err := fsCall("unlink", path) | |
| return err | |
| } | |
| func Rmdir(path string) error { | |
| if err := checkPath(path); err != nil { | |
| return err | |
| } | |
| _, err := fsCall("rmdir", path) | |
| return err | |
| } | |
| func Chmod(path string, mode uint32) error { | |
| if err := checkPath(path); err != nil { | |
| return err | |
| } | |
| _, err := fsCall("chmod", path, mode) | |
| return err | |
| } | |
| func Fchmod(fd int, mode uint32) error { | |
| _, err := fsCall("fchmod", fd, mode) | |
| return err | |
| } | |
| func Chown(path string, uid, gid int) error { | |
| if err := checkPath(path); err != nil { | |
| return err | |
| } | |
| _, err := fsCall("chown", path, uint32(uid), uint32(gid)) | |
| return err | |
| } | |
| func Fchown(fd int, uid, gid int) error { | |
| _, err := fsCall("fchown", fd, uint32(uid), uint32(gid)) | |
| return err | |
| } | |
| func Lchown(path string, uid, gid int) error { | |
| if err := checkPath(path); err != nil { | |
| return err | |
| } | |
| if jsFS.Get("lchown").IsUndefined() { | |
| // fs.lchown is unavailable on Linux until Node.js 10.6.0 | |
| // TODO(neelance): remove when we require at least this Node.js version | |
| return ENOSYS | |
| } | |
| _, err := fsCall("lchown", path, uint32(uid), uint32(gid)) | |
| return err | |
| } | |
| func UtimesNano(path string, ts []Timespec) error { | |
| // UTIME_OMIT value must match internal/syscall/unix/at_js.go | |
| const UTIME_OMIT = -0x2 | |
| if err := checkPath(path); err != nil { | |
| return err | |
| } | |
| if len(ts) != 2 { | |
| return EINVAL | |
| } | |
| atime := ts[0].Sec | |
| mtime := ts[1].Sec | |
| if atime == UTIME_OMIT || mtime == UTIME_OMIT { | |
| var st Stat_t | |
| if err := Stat(path, &st); err != nil { | |
| return err | |
| } | |
| if atime == UTIME_OMIT { | |
| atime = st.Atime | |
| } | |
| if mtime == UTIME_OMIT { | |
| mtime = st.Mtime | |
| } | |
| } | |
| _, err := fsCall("utimes", path, atime, mtime) | |
| return err | |
| } | |
| func Rename(from, to string) error { | |
| if err := checkPath(from); err != nil { | |
| return err | |
| } | |
| if err := checkPath(to); err != nil { | |
| return err | |
| } | |
| _, err := fsCall("rename", from, to) | |
| return err | |
| } | |
| func Truncate(path string, length int64) error { | |
| if err := checkPath(path); err != nil { | |
| return err | |
| } | |
| _, err := fsCall("truncate", path, length) | |
| return err | |
| } | |
| func Ftruncate(fd int, length int64) error { | |
| _, err := fsCall("ftruncate", fd, length) | |
| return err | |
| } | |
| func Getcwd(buf []byte) (n int, err error) { | |
| defer recoverErr(&err) | |
| cwd := jsProcess.Call("cwd").String() | |
| n = copy(buf, cwd) | |
| return | |
| } | |
| func Chdir(path string) (err error) { | |
| if err := checkPath(path); err != nil { | |
| return err | |
| } | |
| defer recoverErr(&err) | |
| jsProcess.Call("chdir", path) | |
| return | |
| } | |
| func Fchdir(fd int) error { | |
| f, err := fdToFile(fd) | |
| if err != nil { | |
| return err | |
| } | |
| return Chdir(f.path) | |
| } | |
| func Readlink(path string, buf []byte) (n int, err error) { | |
| if err := checkPath(path); err != nil { | |
| return 0, err | |
| } | |
| dst, err := fsCall("readlink", path) | |
| if err != nil { | |
| return 0, err | |
| } | |
| n = copy(buf, dst.String()) | |
| return n, nil | |
| } | |
| func Link(path, link string) error { | |
| if err := checkPath(path); err != nil { | |
| return err | |
| } | |
| if err := checkPath(link); err != nil { | |
| return err | |
| } | |
| _, err := fsCall("link", path, link) | |
| return err | |
| } | |
| func Symlink(path, link string) error { | |
| if err := checkPath(path); err != nil { | |
| return err | |
| } | |
| if err := checkPath(link); err != nil { | |
| return err | |
| } | |
| _, err := fsCall("symlink", path, link) | |
| return err | |
| } | |
| func Fsync(fd int) error { | |
| _, err := fsCall("fsync", fd) | |
| return err | |
| } | |
| func Read(fd int, b []byte) (int, error) { | |
| f, err := fdToFile(fd) | |
| if err != nil { | |
| return 0, err | |
| } | |
| if f.seeked { | |
| n, err := Pread(fd, b, f.pos) | |
| f.pos += int64(n) | |
| return n, err | |
| } | |
| buf := uint8Array.New(len(b)) | |
| n, err := fsCall("read", fd, buf, 0, len(b), nil) | |
| if err != nil { | |
| return 0, err | |
| } | |
| n2 := n.Int() | |
| if n2 > 0 { | |
| js.CopyBytesToGo(b[:n2], buf) | |
| } | |
| f.pos += int64(n2) | |
| return n2, err | |
| } | |
| func Write(fd int, b []byte) (int, error) { | |
| f, err := fdToFile(fd) | |
| if err != nil { | |
| return 0, err | |
| } | |
| if f.seeked { | |
| n, err := Pwrite(fd, b, f.pos) | |
| f.pos += int64(n) | |
| return n, err | |
| } | |
| if faketime && (fd == 1 || fd == 2) { | |
| n := faketimeWrite(fd, b) | |
| if n < 0 { | |
| return 0, errnoErr(Errno(-n)) | |
| } | |
| return n, nil | |
| } | |
| buf := uint8Array.New(len(b)) | |
| js.CopyBytesToJS(buf, b) | |
| n, err := fsCall("write", fd, buf, 0, len(b), nil) | |
| if err != nil { | |
| return 0, err | |
| } | |
| n2 := n.Int() | |
| f.pos += int64(n2) | |
| return n2, err | |
| } | |
| func Pread(fd int, b []byte, offset int64) (int, error) { | |
| buf := uint8Array.New(len(b)) | |
| n, err := fsCall("read", fd, buf, 0, len(b), offset) | |
| if err != nil { | |
| return 0, err | |
| } | |
| n2 := n.Int() | |
| if n2 > 0 { | |
| js.CopyBytesToGo(b[:n2], buf) | |
| } | |
| return n2, nil | |
| } | |
| func Pwrite(fd int, b []byte, offset int64) (int, error) { | |
| buf := uint8Array.New(len(b)) | |
| js.CopyBytesToJS(buf, b) | |
| n, err := fsCall("write", fd, buf, 0, len(b), offset) | |
| if err != nil { | |
| return 0, err | |
| } | |
| return n.Int(), nil | |
| } | |
| func Seek(fd int, offset int64, whence int) (int64, error) { | |
| f, err := fdToFile(fd) | |
| if err != nil { | |
| return 0, err | |
| } | |
| var newPos int64 | |
| switch whence { | |
| case 0: | |
| newPos = offset | |
| case 1: | |
| newPos = f.pos + offset | |
| case 2: | |
| var st Stat_t | |
| if err := Fstat(fd, &st); err != nil { | |
| return 0, err | |
| } | |
| newPos = st.Size + offset | |
| default: | |
| return 0, errnoErr(EINVAL) | |
| } | |
| if newPos < 0 { | |
| return 0, errnoErr(EINVAL) | |
| } | |
| f.seeked = true | |
| f.dirIdx = 0 // Reset directory read position. See issue 35767. | |
| f.pos = newPos | |
| return newPos, nil | |
| } | |
| func Dup(fd int) (int, error) { | |
| return 0, ENOSYS | |
| } | |
| func Dup2(fd, newfd int) error { | |
| return ENOSYS | |
| } | |
| func Pipe(fd []int) error { | |
| return ENOSYS | |
| } | |
| func fsCall(name string, args ...any) (js.Value, error) { | |
| type callResult struct { | |
| val js.Value | |
| err error | |
| } | |
| c := make(chan callResult, 1) | |
| f := js.FuncOf(func(this js.Value, args []js.Value) any { | |
| var res callResult | |
| if len(args) >= 1 { // on Node.js 8, fs.utimes calls the callback without any arguments | |
| if jsErr := args[0]; !jsErr.IsNull() { | |
| res.err = mapJSError(jsErr) | |
| } | |
| } | |
| res.val = js.Undefined() | |
| if len(args) >= 2 { | |
| res.val = args[1] | |
| } | |
| c <- res | |
| return nil | |
| }) | |
| defer f.Release() | |
| jsFS.Call(name, append(args, f)...) | |
| res := <-c | |
| return res.val, res.err | |
| } | |
| // checkPath checks that the path is not empty and that it contains no null characters. | |
| func checkPath(path string) error { | |
| if path == "" { | |
| return EINVAL | |
| } | |
| for i := 0; i < len(path); i++ { | |
| if path[i] == '\x00' { | |
| return EINVAL | |
| } | |
| } | |
| return nil | |
| } | |
| func recoverErr(errPtr *error) { | |
| if err := recover(); err != nil { | |
| jsErr, ok := err.(js.Error) | |
| if !ok { | |
| panic(err) | |
| } | |
| *errPtr = mapJSError(jsErr.Value) | |
| } | |
| } | |
| // mapJSError maps an error given by Node.js to the appropriate Go error. | |
| func mapJSError(jsErr js.Value) error { | |
| errno, ok := errnoByCode[jsErr.Get("code").String()] | |
| if !ok { | |
| panic(jsErr) | |
| } | |
| return errnoErr(Errno(errno)) | |
| } |