diff --git a/.github/actions/publish-types/action.yml b/.github/actions/publish-types/action.yml new file mode 100644 index 0000000..10c98a7 --- /dev/null +++ b/.github/actions/publish-types/action.yml @@ -0,0 +1,48 @@ +name: Publish type definitions to npm +inputs: + VERSION: + description: Packages version + required: true + NODE_AUTH_TOKEN: + description: Node auth token + required: true +runs: + using: composite + steps: + - name: Set variables + id: vars + shell: bash + run: | + echo "PKGS=buffer url" >> "${GITHUB_OUTPUT}" + + - name: Set versions + shell: bash + run: | + jq --arg version "${{ inputs.VERSION }}" '.version = $version' global-types/package.json > global-types/tmp.json && mv global-types/tmp.json global-types/package.json + for pkg in ${{ steps.vars.outputs.PKGS }}; do + jq --arg version "${{ inputs.VERSION }}" '.version = $version' $pkg/types/package.json > $pkg/types/tmp.json && mv $pkg/types/tmp.json $pkg/types/package.json + done + - name: Update dependency versions + shell: bash + run: | + for pkg in ${{ steps.vars.outputs.PKGS }}; do + jq --arg version "${{ inputs.VERSION }}" '.dependencies."@dop251/types-goja_nodejs-global" = $version' $pkg/types/package.json > $pkg/types/tmp.json && mv $pkg/types/tmp.json $pkg/types/package.json + done + - name: Setup nodejs + uses: actions/setup-node@v2 + with: + node-version: '22' + registry-url: 'https://registry.npmjs.org' + - name: Publish the packages + shell: bash + run: | + cd global-types + npm publish --access=public + cd - + for pkg in ${{ steps.vars.outputs.PKGS }}; do + cd $pkg/types + npm publish --access=public + cd - + done + env: + NODE_AUTH_TOKEN: ${{ inputs.NODE_AUTH_TOKEN }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..701f305 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,79 @@ +on: [push, pull_request] +name: Test +jobs: + test: + strategy: + matrix: + go-version: [1.20.x, 1.x] + os: [ubuntu-latest, windows-latest] + arch: ["", "386"] + fail-fast: false + runs-on: ${{ matrix.os }} + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v3 + - name: Check formatting + run: diff -u <(echo -n) <(gofmt -d .) + if: runner.os != 'Windows' + - name: Run go vet + env: + GOARCH: ${{ matrix.arch }} + run: go vet ./... + - name: Run staticcheck + uses: dominikh/staticcheck-action@v1.3.1 + with: + version: "2025.1.1" + install-go: false + cache-key: ${{ matrix.go-version }} + if: ${{ matrix.go-version == '1.x' }} + - name: Run tests + env: + GOARCH: ${{ matrix.arch }} + run: go test -vet=off ./... + + test-types: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: | + npm ci + npm test --workspaces + + publish-types-tagged: + name: 'Publish type definitions to npm (tagged)' + needs: [test, test-types] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + steps: + - uses: actions/checkout@v4 + - name: Extract version + run: | + VERSION=$(echo "${{ github.ref }}" | sed 's|refs/tags/v||') + if [[ "$VERSION" == [0-9].* ]]; then + echo "version=$VERSION" >> $GITHUB_ENV + fi + - uses: './.github/actions/publish-types' + if: env.version != '' + with: + VERSION: ${{ env.version }} + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} + + publish-types-untagged: + name: 'Publish type definitions to npm (untagged master)' + needs: [ test, test-types ] + runs-on: ubuntu-latest + if: "!startsWith(github.ref, 'refs/tags/v') && github.ref_name == 'master'" + steps: + - uses: actions/checkout@v4 + - name: Extract version + run: | + VERSION=0.0.0-$(git log -1 --format=%cd --date=format:%Y%m%d%H%M%S)-$(git rev-parse --short=12 HEAD) + echo "version=$VERSION" >> $GITHUB_ENV + - uses: './.github/actions/publish-types' + with: + VERSION: ${{ env.version }} + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bca8bca --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +*.iml +vendor/* +node_modules diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8c27a94 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2016 Dmitry Panov + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9872165 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +Nodejs compatibility library for Goja +==== + +This is a collection of [Goja](https://github.com/dop251/goja) modules that provide nodejs compatibility. + +Example: + +```go +package main + +import ( + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/require" +) + +func main() { + registry := new(require.Registry) // this can be shared by multiple runtimes + + runtime := goja.New() + req := registry.Enable(runtime) + + runtime.RunString(` + var m = require("./m.js"); + m.test(); + `) + + m, err := req.Require("./m.js") + _, _ = m, err +} +``` + +Type Definitions +--- + +Type definitions are published to https://npmjs.com as @dop251/types-goja_nodejs-MODULE. +They only include what's been implemented so far. + +To make use of them you need to install the appropriate modules and add `node_modules/@dop251` to `typeRoots` in `tsconfig.json`. + +I didn't want to add those to DefinitelyTyped partly because I don't think they really belong there, +and partly because I'd like to fully control the release cycle, i.e. publish the modules by an automated CI job and +exactly at the same time as the Go code is released. + +And the reason for splitting them into different packages is that the modules can be enabled or disabled individually, unlike in nodejs. + +More modules will be added. Contributions welcome too. diff --git a/assert.js b/assert.js new file mode 100644 index 0000000..6fda5f9 --- /dev/null +++ b/assert.js @@ -0,0 +1,129 @@ +'use strict'; + +const assert = { + _isSameValue(a, b) { + if (this._isNumber(a)) { + return this._numberEquals(a, b); + } + + return a === b; + }, + + _isNumber(val) { + return typeof val === "number"; + }, + + _toString(value) { + try { + if (value === 0 && 1 / value === -Infinity) { + return '-0'; + } + + return String(value); + } catch (err) { + if (err.name === 'TypeError') { + return Object.prototype.toString.call(value); + } + + throw err; + } + }, + + _numberEquals(a, b, precision = 1e-6) { + if (!this._isNumber(b)) { + return false; + } + // Handle NaN vs. NaN + if (a !== a && b !== b) { + return true; // Both are NaN + } + // If only one is NaN, they're not equal + if (a !== a || b !== b) { + return false; + } + if (a === b) { + // Handle +/-0 vs. -/+0 + return a !== 0 || 1 / a === 1 / b; + } + // Use relative error for larger numbers, absolute for smaller ones + if (Math.abs(a) > 1 || Math.abs(b) > 1) { + return Math.abs((a - b) / Math.max(Math.abs(a), Math.abs(b))) < precision; + } + + // Absolute error for small numbers + return Math.abs(a - b) < precision; + }, + + sameValue(actual, expected, message) { + if (assert._isSameValue(actual, expected)) { + return; + } + if (message === undefined) { + message = ''; + } else { + message += ' '; + } + + message += 'Expected SameValue(«' + assert._toString(actual) + '», «' + assert._toString(expected) + '») to be true'; + + throw new Error(message); + }, + + _throws(f, checks, message) { + if (message === undefined) { + message = ''; + } else { + message += ' '; + } + try { + f(); + } catch (e) { + for (const check of checks) { + check(e, message); + } + return; + } + throw new Error(message + "No exception was thrown"); + }, + + _sameErrorType(expected){ + return function(e, message) { + assert.sameValue(e.constructor, expected, `${message}Wrong exception type was thrown:`); + } + }, + + _sameErrorCode(expected){ + return function(e, message) { + assert.sameValue(e.code, expected, `${message}Wrong exception code was thrown:`); + } + }, + + _sameErrorMessage(expected){ + return function(e, message) { + assert.sameValue(e.message, expected, `${message}Wrong exception message was thrown:`); + } + }, + + throws(f, ctor, message) { + return this._throws(f, [ + this._sameErrorType(ctor) + ], message); + }, + + throwsNodeError(f, ctor, code, message) { + return this._throws(f, [ + this._sameErrorType(ctor), + this._sameErrorCode(code) + ], message); + }, + + throwsNodeErrorWithMessage(f, ctor, code, errorMessage, message) { + return this._throws(f, [ + this._sameErrorType(ctor), + this._sameErrorCode(code), + this._sameErrorMessage(errorMessage) + ], message); + } +} + +module.exports = assert; \ No newline at end of file diff --git a/buffer/buffer.go b/buffer/buffer.go new file mode 100644 index 0000000..f7d2bdc --- /dev/null +++ b/buffer/buffer.go @@ -0,0 +1,1198 @@ +package buffer + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "math" + "math/big" + "reflect" + "strconv" + + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/errors" + "github.com/dop251/goja_nodejs/goutil" + "github.com/dop251/goja_nodejs/require" + + "github.com/dop251/base64dec" + "golang.org/x/text/encoding/unicode" +) + +const ModuleName = "buffer" + +type Buffer struct { + r *goja.Runtime + + bufferCtorObj *goja.Object + + uint8ArrayCtorObj *goja.Object + uint8ArrayCtor goja.Constructor +} + +var ( + symApi = goja.NewSymbol("api") +) + +var ( + reflectTypeArrayBuffer = reflect.TypeOf(goja.ArrayBuffer{}) + reflectTypeString = reflect.TypeOf("") + reflectTypeInt = reflect.TypeOf(int64(0)) + reflectTypeFloat = reflect.TypeOf(0.0) + reflectTypeBytes = reflect.TypeOf(([]byte)(nil)) +) + +func Enable(runtime *goja.Runtime) { + runtime.Set("Buffer", require.Require(runtime, ModuleName).ToObject(runtime).Get("Buffer")) +} + +func Bytes(r *goja.Runtime, v goja.Value) []byte { + var b []byte + err := r.ExportTo(v, &b) + if err != nil { + return []byte(v.String()) + } + return b +} + +func mod(r *goja.Runtime) *goja.Object { + res := r.Get("Buffer") + if res == nil { + res = require.Require(r, ModuleName).ToObject(r).Get("Buffer") + } + m, ok := res.(*goja.Object) + if !ok { + panic(r.NewTypeError("Could not extract Buffer")) + } + return m +} + +func api(mod *goja.Object) *Buffer { + if s := mod.GetSymbol(symApi); s != nil { + b, _ := s.Export().(*Buffer) + return b + } + + return nil +} + +func GetApi(r *goja.Runtime) *Buffer { + return api(mod(r)) +} + +func DecodeBytes(r *goja.Runtime, arg, enc goja.Value) []byte { + switch arg.ExportType() { + case reflectTypeArrayBuffer: + return arg.Export().(goja.ArrayBuffer).Bytes() + case reflectTypeString: + var codec StringCodec + if !goja.IsUndefined(enc) { + codec = stringCodecs[enc.String()] + } + if codec == nil { + codec = utf8Codec + } + return codec.DecodeAppend(arg.String(), nil) + default: + if o, ok := arg.(*goja.Object); ok { + if o.ExportType() == reflectTypeBytes { + return o.Export().([]byte) + } + } + } + panic(errors.NewTypeError(r, errors.ErrCodeInvalidArgType, "The \"data\" argument must be of type string or an instance of Buffer, TypedArray, or DataView.")) +} + +func WrapBytes(r *goja.Runtime, data []byte) *goja.Object { + m := mod(r) + if api := api(m); api != nil { + return api.WrapBytes(data) + } + if from, ok := goja.AssertFunction(m.Get("from")); ok { + ab := r.NewArrayBuffer(data) + v, err := from(m, r.ToValue(ab)) + if err != nil { + panic(err) + } + return v.ToObject(r) + } + panic(r.NewTypeError("Buffer.from is not a function")) +} + +// EncodeBytes returns the given byte slice encoded as string with the given encoding. If encoding +// is not specified or not supported, returns a Buffer that wraps the data. +func EncodeBytes(r *goja.Runtime, data []byte, enc goja.Value) goja.Value { + var codec StringCodec + if !goja.IsUndefined(enc) { + codec = StringCodecByName(enc.String()) + } + if codec != nil { + return r.ToValue(codec.Encode(data)) + } + return WrapBytes(r, data) +} + +func (b *Buffer) WrapBytes(data []byte) *goja.Object { + return b.fromBytes(data) +} + +func (b *Buffer) ctor(call goja.ConstructorCall) (res *goja.Object) { + arg := call.Argument(0) + switch arg.ExportType() { + case reflectTypeInt, reflectTypeFloat: + panic(b.r.NewTypeError("Calling the Buffer constructor with numeric argument is not implemented yet")) + // TODO implement + } + return b._from(call.Arguments...) +} + +type StringCodec interface { + DecodeAppend(string, []byte) []byte + Encode([]byte) string + Decode(s string) []byte +} + +type hexCodec struct{} + +func (hexCodec) DecodeAppend(s string, b []byte) []byte { + l := hex.DecodedLen(len(s)) + dst, res := expandSlice(b, l) + n, err := hex.Decode(dst, []byte(s)) + if err != nil { + res = res[:len(b)+n] + } + return res +} + +func (hexCodec) Decode(s string) []byte { + n, _ := hex.DecodeString(s) + return n +} +func (hexCodec) Encode(b []byte) string { + return hex.EncodeToString(b) +} + +type _utf8Codec struct{} + +func (c _utf8Codec) DecodeAppend(s string, b []byte) []byte { + r := c.Decode(s) + dst, res := expandSlice(b, len(r)) + copy(dst, r) + return res +} + +func (_utf8Codec) Decode(s string) []byte { + r, _ := unicode.UTF8.NewEncoder().String(s) + return []byte(r) +} +func (_utf8Codec) Encode(b []byte) string { + r, _ := unicode.UTF8.NewDecoder().Bytes(b) + return string(r) +} + +type base64Codec struct{} + +type base64UrlCodec struct { + base64Codec +} + +func (base64Codec) DecodeAppend(s string, b []byte) []byte { + res, _ := Base64DecodeAppend(b, s) + return res +} + +func (base64Codec) Decode(s string) []byte { + res, _ := base64.StdEncoding.DecodeString(s) + return res +} +func (base64Codec) Encode(b []byte) string { + return base64.StdEncoding.EncodeToString(b) +} + +func (base64UrlCodec) Encode(b []byte) string { + return base64.RawURLEncoding.EncodeToString(b) +} + +var utf8Codec StringCodec = _utf8Codec{} + +var stringCodecs = map[string]StringCodec{ + "hex": hexCodec{}, + "utf8": utf8Codec, + "utf-8": utf8Codec, + "base64": base64Codec{}, + "base64Url": base64UrlCodec{}, +} + +func expandSlice(b []byte, l int) (dst, res []byte) { + if cap(b)-len(b) < l { + b1 := make([]byte, len(b)+l) + copy(b1, b) + dst = b1[len(b):] + res = b1 + } else { + dst = b[len(b) : len(b)+l] + res = b[:len(b)+l] + } + return +} + +func Base64DecodeAppend(dst []byte, src string) ([]byte, error) { + l := base64.RawStdEncoding.DecodedLen(len(src)) + d, res := expandSlice(dst, l) + n, err := base64dec.DecodeBase64(d, src) + + res = res[:len(dst)+n] + return res, err +} + +func (b *Buffer) fromString(str, enc string) *goja.Object { + codec := stringCodecs[enc] + if codec == nil { + codec = utf8Codec + } + return b.fromBytes(codec.DecodeAppend(str, nil)) +} + +func (b *Buffer) fromBytes(data []byte) *goja.Object { + o, err := b.uint8ArrayCtor(b.bufferCtorObj, b.r.ToValue(b.r.NewArrayBuffer(data))) + if err != nil { + panic(err) + } + return o +} + +func (b *Buffer) _from(args ...goja.Value) *goja.Object { + if len(args) == 0 { + panic(errors.NewTypeError(b.r, errors.ErrCodeInvalidArgType, "The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received undefined")) + } + arg := args[0] + switch arg.ExportType() { + case reflectTypeArrayBuffer: + v, err := b.uint8ArrayCtor(b.bufferCtorObj, args...) + if err != nil { + panic(err) + } + return v + case reflectTypeString: + var enc string + if len(args) > 1 { + enc = args[1].String() + } + return b.fromString(arg.String(), enc) + default: + if o, ok := arg.(*goja.Object); ok { + if o.ExportType() == reflectTypeBytes { + bb, _ := o.Export().([]byte) + a := make([]byte, len(bb)) + copy(a, bb) + return b.fromBytes(a) + } else { + if f, ok := goja.AssertFunction(o.Get("valueOf")); ok { + valueOf, err := f(o) + if err != nil { + panic(err) + } + if valueOf != o { + args[0] = valueOf + return b._from(args...) + } + } + + if s := o.GetSymbol(goja.SymToPrimitive); s != nil { + if f, ok := goja.AssertFunction(s); ok { + str, err := f(o, b.r.ToValue("string")) + if err != nil { + panic(err) + } + args[0] = str + return b._from(args...) + } + } + } + // array-like + if v := o.Get("length"); v != nil { + length := int(v.ToInteger()) + a := make([]byte, length) + for i := 0; i < length; i++ { + item := o.Get(strconv.Itoa(i)) + if item != nil { + a[i] = byte(item.ToInteger()) + } + } + return b.fromBytes(a) + } + } + } + panic(errors.NewTypeError(b.r, errors.ErrCodeInvalidArgType, "The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received %s", arg)) +} + +func (b *Buffer) from(call goja.FunctionCall) goja.Value { + return b._from(call.Arguments...) +} + +func StringCodecByName(name string) StringCodec { + return stringCodecs[name] +} + +func (b *Buffer) getStringCodec(enc goja.Value) (codec StringCodec) { + if !goja.IsUndefined(enc) { + codec = stringCodecs[enc.String()] + if codec == nil { + panic(errors.NewTypeError(b.r, "ERR_UNKNOWN_ENCODING", "Unknown encoding: %s", enc)) + } + } else { + codec = utf8Codec + } + return +} + +func (b *Buffer) fill(buf []byte, fill string, enc goja.Value) []byte { + codec := b.getStringCodec(enc) + b1 := codec.DecodeAppend(fill, buf[:0]) + if len(b1) > len(buf) { + return b1[:len(buf)] + } + for i := len(b1); i < len(buf); { + i += copy(buf[i:], buf[:i]) + } + return buf +} + +func (b *Buffer) alloc(call goja.FunctionCall) goja.Value { + arg0 := call.Argument(0) + size := -1 + if goja.IsNumber(arg0) { + size = int(arg0.ToInteger()) + } + if size < 0 { + panic(errors.NewArgumentNotNumberTypeError(b.r, "size")) + } + fill := call.Argument(1) + buf := make([]byte, size) + if !goja.IsUndefined(fill) { + if goja.IsString(fill) { + var enc goja.Value + if a := call.Argument(2); goja.IsString(a) { + enc = a + } else { + enc = goja.Undefined() + } + buf = b.fill(buf, fill.String(), enc) + } else { + fill = fill.ToNumber() + if !goja.IsNaN(fill) && !goja.IsInfinity(fill) { + fillByte := byte(fill.ToInteger()) + if fillByte != 0 { + for i := range buf { + buf[i] = fillByte + } + } + } + } + } + return b.fromBytes(buf) +} + +func (b *Buffer) proto_toString(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + codec := b.getStringCodec(call.Argument(0)) + start := goutil.CoercedIntegerArgument(call, 1, 0, 0) + + // Node's Buffer class makes this zero if it is negative + if start < 0 { + start = 0 + } else if start >= int64(len(bb)) { + // returns an empty string if start is beyond the length of the buffer + return b.r.ToValue("") + } + + // NOTE that Node will default to the length of the buffer, but uses 0 for type mismatch defaults + end := goutil.CoercedIntegerArgument(call, 2, int64(len(bb)), 0) + if end < 0 || start >= end { + // returns an empty string if end < 0 or start >= end + return b.r.ToValue("") + } else if end > int64(len(bb)) { + // and Node ensures you don't go past the Buffer + end = int64(len(bb)) + } + + return b.r.ToValue(codec.Encode(bb[start:end])) +} + +func (b *Buffer) proto_equals(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + other := call.Argument(0) + if b.r.InstanceOf(other, b.uint8ArrayCtorObj) { + otherBytes := Bytes(b.r, other) + return b.r.ToValue(bytes.Equal(bb, otherBytes)) + } + panic(errors.NewTypeError(b.r, errors.ErrCodeInvalidArgType, "The \"otherBuffer\" argument must be an instance of Buffer or Uint8Array.")) +} + +// readBigInt64BE reads a big-endian 64-bit signed integer from the buffer +func (b *Buffer) readBigInt64BE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset := b.getOffsetArgument(call, 0, bb, 8) + value := int64(binary.BigEndian.Uint64(bb[offset : offset+8])) + + return b.r.ToValue(big.NewInt(value)) +} + +// readBigInt64LE reads a little-endian 64-bit signed integer from the buffer +func (b *Buffer) readBigInt64LE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset := b.getOffsetArgument(call, 0, bb, 8) + value := int64(binary.LittleEndian.Uint64(bb[offset : offset+8])) + + return b.r.ToValue(big.NewInt(value)) +} + +// readBigUInt64BE reads a big-endian 64-bit unsigned integer from the buffer +func (b *Buffer) readBigUInt64BE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset := b.getOffsetArgument(call, 0, bb, 8) + value := binary.BigEndian.Uint64(bb[offset : offset+8]) + + return b.r.ToValue(new(big.Int).SetUint64(value)) +} + +// readBigUInt64LE reads a little-endian 64-bit unsigned integer from the buffer +func (b *Buffer) readBigUInt64LE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset := b.getOffsetArgument(call, 0, bb, 8) + value := binary.LittleEndian.Uint64(bb[offset : offset+8]) + + return b.r.ToValue(new(big.Int).SetUint64(value)) +} + +// readDoubleBE reads a big-endian 64-bit floating-point number from the buffer +func (b *Buffer) readDoubleBE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset := b.getOffsetArgument(call, 0, bb, 8) + value := binary.BigEndian.Uint64(bb[offset : offset+8]) + + return b.r.ToValue(math.Float64frombits(value)) +} + +// readDoubleLE reads a little-endian 64-bit floating-point number from the buffer +func (b *Buffer) readDoubleLE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset := b.getOffsetArgument(call, 0, bb, 8) + value := binary.LittleEndian.Uint64(bb[offset : offset+8]) + + return b.r.ToValue(math.Float64frombits(value)) +} + +// readFloatBE reads a big-endian 32-bit floating-point number from the buffer +func (b *Buffer) readFloatBE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset := b.getOffsetArgument(call, 0, bb, 4) + value := binary.BigEndian.Uint32(bb[offset : offset+4]) + + return b.r.ToValue(math.Float32frombits(value)) +} + +// readFloatLE reads a little-endian 32-bit floating-point number from the buffer +func (b *Buffer) readFloatLE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset := b.getOffsetArgument(call, 0, bb, 4) + value := binary.LittleEndian.Uint32(bb[offset : offset+4]) + + return b.r.ToValue(math.Float32frombits(value)) +} + +// readInt8 reads an 8-bit signed integer from the buffer +func (b *Buffer) readInt8(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset := b.getOffsetArgument(call, 0, bb, 1) + value := int8(bb[offset]) + + return b.r.ToValue(value) +} + +// readInt16BE reads a big-endian 16-bit signed integer from the buffer +func (b *Buffer) readInt16BE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset := b.getOffsetArgument(call, 0, bb, 2) + value := int16(binary.BigEndian.Uint16(bb[offset : offset+2])) + + return b.r.ToValue(value) +} + +// readInt16LE reads a little-endian 16-bit signed integer from the buffer +func (b *Buffer) readInt16LE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset := b.getOffsetArgument(call, 0, bb, 2) + value := int16(binary.LittleEndian.Uint16(bb[offset : offset+2])) + + return b.r.ToValue(value) +} + +// readInt32BE reads a big-endian 32-bit signed integer from the buffer +func (b *Buffer) readInt32BE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset := b.getOffsetArgument(call, 0, bb, 4) + value := int32(binary.BigEndian.Uint32(bb[offset : offset+4])) + + return b.r.ToValue(value) +} + +// readInt32LE reads a little-endian 32-bit signed integer from the buffer +func (b *Buffer) readInt32LE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset := b.getOffsetArgument(call, 0, bb, 4) + value := int32(binary.LittleEndian.Uint32(bb[offset : offset+4])) + + return b.r.ToValue(value) +} + +// readIntBE reads a big-endian signed integer of variable byte length +func (b *Buffer) readIntBE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset, byteLength := b.getVariableLengthReadArguments(call, bb) + + var value int64 + for i := int64(0); i < byteLength; i++ { + value = (value << 8) | int64(bb[offset+i]) + } + + value = signExtend(value, byteLength) + + return b.r.ToValue(value) +} + +// readIntLE reads a little-endian signed integer of variable byte length +func (b *Buffer) readIntLE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset, byteLength := b.getVariableLengthReadArguments(call, bb) + + var value int64 + for i := byteLength - 1; i >= 0; i-- { + value = (value << 8) | int64(bb[offset+i]) + } + + value = signExtend(value, byteLength) + + return b.r.ToValue(value) +} + +// readUInt8 reads an 8-bit unsigned integer from the buffer +func (b *Buffer) readUInt8(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset := b.getOffsetArgument(call, 0, bb, 1) + value := bb[offset] + + return b.r.ToValue(value) +} + +// readUInt16BE reads a big-endian 16-bit unsigned integer from the buffer +func (b *Buffer) readUInt16BE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset := b.getOffsetArgument(call, 0, bb, 2) + value := binary.BigEndian.Uint16(bb[offset : offset+2]) + + return b.r.ToValue(value) +} + +// readUInt16LE reads a little-endian 16-bit unsigned integer from the buffer +func (b *Buffer) readUInt16LE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset := b.getOffsetArgument(call, 0, bb, 2) + value := binary.LittleEndian.Uint16(bb[offset : offset+2]) + + return b.r.ToValue(value) +} + +// readUInt32BE reads a big-endian 32-bit unsigned integer from the buffer +func (b *Buffer) readUInt32BE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset := b.getOffsetArgument(call, 0, bb, 4) + value := binary.BigEndian.Uint32(bb[offset : offset+4]) + + return b.r.ToValue(value) +} + +// readUInt32LE reads a little-endian 32-bit unsigned integer from the buffer +func (b *Buffer) readUInt32LE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset := b.getOffsetArgument(call, 0, bb, 4) + value := binary.LittleEndian.Uint32(bb[offset : offset+4]) + + return b.r.ToValue(value) +} + +// readUIntBE reads a big-endian unsigned integer of variable byte length +func (b *Buffer) readUIntBE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset, byteLength := b.getVariableLengthReadArguments(call, bb) + + var value uint64 + for i := int64(0); i < byteLength; i++ { + value = (value << 8) | uint64(bb[offset+i]) + } + + return b.r.ToValue(value) +} + +// readUIntLE reads a little-endian unsigned integer of variable byte length +func (b *Buffer) readUIntLE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + offset, byteLength := b.getVariableLengthReadArguments(call, bb) + + var value uint64 + for i := byteLength - 1; i >= 0; i-- { + value = (value << 8) | uint64(bb[offset+i]) + } + + return b.r.ToValue(value) +} + +// write will write a string to the Buffer at offset according to the character encoding. The length parameter is +// the number of bytes to write. If buffer did not contain enough space to fit the entire string, only part of string +// will be written. +func (b *Buffer) write(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + str := goutil.RequiredStringArgument(b.r, call, "string", 0) + // note that we are passing in zero for numBytes, since the length parameter, which depends on offset, + // will dictate the number of bytes + offset := b.getOffsetArgument(call, 1, bb, 0) + // the length defaults to the size of the buffer - offset + maxLength := int64(len(bb)) - offset + length := goutil.OptionalIntegerArgument(b.r, call, "length", 2, maxLength) + codec := b.getStringCodec(call.Argument(3)) + + raw := codec.Decode(str) + if int64(len(raw)) < length { + // make sure we only write up to raw bytes + length = int64(len(raw)) + } + n := copy(bb[offset:], raw[:length]) + return b.r.ToValue(n) +} + +// writeBigInt64BE writes a big-endian 64-bit signed integer to the buffer +func (b *Buffer) writeBigInt64BE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredBigIntArgument(b.r, call, "value", 0) + offset := b.getOffsetArgument(call, 1, bb, 8) + + intValue := value.Int64() + binary.BigEndian.PutUint64(bb[offset:offset+8], uint64(intValue)) + + return b.r.ToValue(offset + 8) +} + +// writeBigInt64LE writes a little-endian 64-bit signed integer to the buffer +func (b *Buffer) writeBigInt64LE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredBigIntArgument(b.r, call, "value", 0) + offset := b.getOffsetArgument(call, 1, bb, 8) + + intValue := value.Int64() + binary.LittleEndian.PutUint64(bb[offset:offset+8], uint64(intValue)) + + return b.r.ToValue(offset + 8) +} + +// writeBigUInt64BE writes a big-endian 64-bit unsigned integer to the buffer +func (b *Buffer) writeBigUInt64BE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredBigIntArgument(b.r, call, "value", 0) + offset := b.getOffsetArgument(call, 1, bb, 8) + + uintValue := value.Uint64() + binary.BigEndian.PutUint64(bb[offset:offset+8], uintValue) + + return b.r.ToValue(offset + 8) +} + +// writeBigUInt64LE writes a little-endian 64-bit unsigned integer to the buffer +func (b *Buffer) writeBigUInt64LE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredBigIntArgument(b.r, call, "value", 0) + offset := b.getOffsetArgument(call, 1, bb, 8) + + uintValue := value.Uint64() + binary.LittleEndian.PutUint64(bb[offset:offset+8], uintValue) + + return b.r.ToValue(offset + 8) +} + +// writeDoubleBE writes a big-endian 64-bit double to the buffer +func (b *Buffer) writeDoubleBE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredFloatArgument(b.r, call, "value", 0) + offset := b.getOffsetArgument(call, 1, bb, 8) + + bits := math.Float64bits(value) + binary.BigEndian.PutUint64(bb[offset:offset+8], bits) + + return b.r.ToValue(offset + 8) +} + +// writeDoubleLE writes a little-endian 64-bit double to the buffer +func (b *Buffer) writeDoubleLE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredFloatArgument(b.r, call, "value", 0) + offset := b.getOffsetArgument(call, 1, bb, 8) + + bits := math.Float64bits(value) + binary.LittleEndian.PutUint64(bb[offset:offset+8], bits) + + return b.r.ToValue(offset + 8) +} + +// writeFloatBE writes a big-endian 32-bit float to the buffer +func (b *Buffer) writeFloatBE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredFloatArgument(b.r, call, "value", 0) + offset := b.getOffsetArgument(call, 1, bb, 4) + + b.ensureWithinFloat32Range(value) + + bits := math.Float32bits(float32(value)) + binary.BigEndian.PutUint32(bb[offset:offset+4], bits) + + return b.r.ToValue(offset + 4) +} + +// writeFloatLE writes a little-endian 32-bit floating-point number to the buffer +func (b *Buffer) writeFloatLE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredFloatArgument(b.r, call, "value", 0) + offset := b.getOffsetArgument(call, 1, bb, 4) + + b.ensureWithinFloat32Range(value) + + bits := math.Float32bits(float32(value)) + binary.LittleEndian.PutUint32(bb[offset:offset+4], bits) + + return b.r.ToValue(offset + 4) +} + +// writeInt8 writes an 8-bit signed integer to the buffer +func (b *Buffer) writeInt8(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredIntegerArgument(b.r, call, "value", 0) + offset := b.getOffsetArgument(call, 1, bb, 1) + + if value < math.MinInt8 || value > math.MaxInt8 { + panic(errors.NewArgumentOutOfRangeError(b.r, "value", value)) + } + + bb[offset] = byte(int8(value)) + + return b.r.ToValue(offset + 1) +} + +// writeInt16BE writes a big-endian 16-bit signed integer to the buffer +func (b *Buffer) writeInt16BE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredIntegerArgument(b.r, call, "value", 0) + offset := b.getOffsetArgument(call, 1, bb, 2) + + b.ensureWithinInt16Range(value) + + binary.BigEndian.PutUint16(bb[offset:offset+2], uint16(value)) + + return b.r.ToValue(offset + 2) +} + +// writeInt16LE writes a little-endian 16-bit signed integer to the buffer +func (b *Buffer) writeInt16LE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredIntegerArgument(b.r, call, "value", 0) + offset := b.getOffsetArgument(call, 1, bb, 2) + + b.ensureWithinInt16Range(value) + + binary.LittleEndian.PutUint16(bb[offset:offset+2], uint16(value)) + + return b.r.ToValue(offset + 2) +} + +// writeInt32BE writes a big-endian 32-bit signed integer to the buffer +func (b *Buffer) writeInt32BE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredIntegerArgument(b.r, call, "value", 0) + offset := b.getOffsetArgument(call, 1, bb, 4) + + b.ensureWithinInt32Range(value) + + binary.BigEndian.PutUint32(bb[offset:offset+4], uint32(value)) + + return b.r.ToValue(offset + 4) +} + +// writeInt32LE writes a little-endian 32-bit signed integer to the buffer +func (b *Buffer) writeInt32LE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredIntegerArgument(b.r, call, "value", 0) + offset := b.getOffsetArgument(call, 1, bb, 4) + + b.ensureWithinInt32Range(value) + + binary.LittleEndian.PutUint32(bb[offset:offset+4], uint32(value)) + + return b.r.ToValue(offset + 4) +} + +// writeIntBE writes a big-endian signed integer of variable byte length +func (b *Buffer) writeIntBE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredIntegerArgument(b.r, call, "value", 0) + offset, byteLength := b.getVariableLengthWriteArguments(call, bb) + + b.ensureWithinIntRange(byteLength, value) + + // Write bytes in big-endian order (most significant byte first) + for i := int64(0); i < byteLength; i++ { + shift := uint(8 * (byteLength - 1 - i)) + bb[offset+i] = byte(value >> shift) + } + + return b.r.ToValue(offset + byteLength) +} + +// writeIntLE writes a little-endian signed integer of variable byte length +func (b *Buffer) writeIntLE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredIntegerArgument(b.r, call, "value", 0) + offset, byteLength := b.getVariableLengthWriteArguments(call, bb) + + b.ensureWithinIntRange(byteLength, value) + + // Write bytes in little-endian order + for i := int64(0); i < byteLength; i++ { + shift := uint(8 * i) + bb[offset+i] = byte(value >> shift) + } + + return b.r.ToValue(offset + byteLength) +} + +// writeUInt8 writes an 8-bit unsigned integer to the buffer +func (b *Buffer) writeUInt8(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredIntegerArgument(b.r, call, "value", 0) + offset := b.getOffsetArgument(call, 1, bb, 1) + + if value < 0 || value > 255 { + panic(errors.NewArgumentOutOfRangeError(b.r, "value", value)) + } + + bb[offset] = uint8(value) + + return b.r.ToValue(offset + 1) +} + +// writeUInt16BE writes a big-endian 16-bit unsigned integer to the buffer +func (b *Buffer) writeUInt16BE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredIntegerArgument(b.r, call, "value", 0) + offset := b.getOffsetArgument(call, 1, bb, 2) + + b.ensureWithinUInt16Range(value) + + binary.BigEndian.PutUint16(bb[offset:offset+2], uint16(value)) + + return b.r.ToValue(offset + 2) +} + +// writeUInt16LE writes a little-endian 16-bit unsigned integer to the buffer +func (b *Buffer) writeUInt16LE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredIntegerArgument(b.r, call, "value", 0) + offset := b.getOffsetArgument(call, 1, bb, 2) + + b.ensureWithinUInt16Range(value) + + binary.LittleEndian.PutUint16(bb[offset:offset+2], uint16(value)) + + return b.r.ToValue(offset + 2) +} + +// writeUInt32BE writes a big-endian 32-bit unsigned integer to the buffer +func (b *Buffer) writeUInt32BE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredIntegerArgument(b.r, call, "value", 0) + offset := b.getOffsetArgument(call, 1, bb, 4) + + b.ensureWithinUInt32Range(value) + + binary.BigEndian.PutUint32(bb[offset:offset+4], uint32(value)) + + return b.r.ToValue(offset + 4) +} + +// writeUInt32LE writes a little-endian 32-bit unsigned integer to the buffer +func (b *Buffer) writeUInt32LE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredIntegerArgument(b.r, call, "value", 0) + offset := b.getOffsetArgument(call, 1, bb, 4) + + b.ensureWithinUInt32Range(value) + + binary.LittleEndian.PutUint32(bb[offset:offset+4], uint32(value)) + + return b.r.ToValue(offset + 4) +} + +// writeUIntBE writes a big-endian unsigned integer of variable byte length +func (b *Buffer) writeUIntBE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredIntegerArgument(b.r, call, "value", 0) + offset, byteLength := b.getVariableLengthWriteArguments(call, bb) + + b.ensureWithinUIntRange(byteLength, value) + + // Write the bytes in big-endian order (most significant byte first) + for i := int64(0); i < byteLength; i++ { + shift := (byteLength - 1 - i) * 8 + bb[offset+i] = byte(value >> shift) + } + + return b.r.ToValue(offset + byteLength) +} + +// writeUIntLE writes a little-endian unsigned integer of variable byte length +func (b *Buffer) writeUIntLE(call goja.FunctionCall) goja.Value { + bb := Bytes(b.r, call.This) + value := goutil.RequiredIntegerArgument(b.r, call, "value", 0) + offset, byteLength := b.getVariableLengthWriteArguments(call, bb) + + b.ensureWithinUIntRange(byteLength, value) + + // Write the bytes in little-endian order + for i := int64(0); i < byteLength; i++ { + shift := uint(8 * i) + bb[offset+i] = byte(value >> shift) + } + + return b.r.ToValue(offset + byteLength) +} + +func (b *Buffer) getOffsetArgument(call goja.FunctionCall, argIndex int, bb []byte, numBytes int64) int64 { + offset := goutil.OptionalIntegerArgument(b.r, call, "offset", argIndex, 0) + + if offset < 0 || offset+numBytes > int64(len(bb)) { + panic(errors.NewArgumentOutOfRangeError(b.r, "offset", offset)) + } + + return offset +} + +func (b *Buffer) getVariableLengthReadArguments(call goja.FunctionCall, bb []byte) (int64, int64) { + return b.getVariableLengthArguments(call, bb, 0, 1) +} + +func (b *Buffer) getVariableLengthWriteArguments(call goja.FunctionCall, bb []byte) (int64, int64) { + return b.getVariableLengthArguments(call, bb, 1, 2) +} + +func (b *Buffer) getVariableLengthArguments(call goja.FunctionCall, bb []byte, offsetArgIndex, byteLengthArgIndex int) (int64, int64) { + offset := goutil.RequiredIntegerArgument(b.r, call, "offset", offsetArgIndex) + byteLength := goutil.RequiredIntegerArgument(b.r, call, "byteLength", byteLengthArgIndex) + + if byteLength < 1 || byteLength > 6 { + panic(errors.NewArgumentOutOfRangeError(b.r, "byteLength", byteLength)) + } + if offset < 0 || offset+byteLength > int64(len(bb)) { + panic(errors.NewArgumentOutOfRangeError(b.r, "offset", offset)) + } + + return offset, byteLength +} + +func (b *Buffer) ensureWithinFloat32Range(value float64) { + if value < -math.MaxFloat32 || value > math.MaxFloat32 { + panic(errors.NewArgumentOutOfRangeError(b.r, "value", value)) + } +} + +func (b *Buffer) ensureWithinInt16Range(value int64) { + if value < math.MinInt16 || value > math.MaxInt16 { + panic(errors.NewArgumentOutOfRangeError(b.r, "value", value)) + } +} + +func (b *Buffer) ensureWithinInt32Range(value int64) { + if value < math.MinInt32 || value > math.MaxInt32 { + panic(errors.NewArgumentOutOfRangeError(b.r, "value", value)) + } +} + +// ensureWithinIntRange checks to make sure that value is within the integer range +// defined by the byteLength. Note that byteLength can be at most 6 bytes, so a +// 48 bit integer is the largest possible value. +func (b *Buffer) ensureWithinIntRange(byteLength, value int64) { + // Calculate the valid range for the given byte length + bits := byteLength * 8 + minValue := -(int64(1) << (bits - 1)) + maxValue := (int64(1) << (bits - 1)) - 1 + + if value < minValue || value > maxValue { + panic(errors.NewArgumentOutOfRangeError(b.r, "value", value)) + } +} + +func (b *Buffer) ensureWithinUInt16Range(value int64) { + if value < 0 || value > math.MaxUint16 { + panic(errors.NewArgumentOutOfRangeError(b.r, "value", value)) + } +} + +func (b *Buffer) ensureWithinUInt32Range(value int64) { + if value < 0 || value > math.MaxUint32 { + panic(errors.NewArgumentOutOfRangeError(b.r, "value", value)) + } +} + +// ensureWithinUIntRange checks to make sure that value is within the unsigned integer +// range defined by the byteLength. Note that byteLength can be at most 6 bytes, so a +// 48 bit unsigned integer is the largest possible value. +func (b *Buffer) ensureWithinUIntRange(byteLength, value int64) { + // Validate that the value is within the valid range for the given byteLength + maxValue := (int64(1) << (8 * byteLength)) - 1 + if value < 0 || value > maxValue { + panic(errors.NewArgumentOutOfRangeError(b.r, "value", value)) + } +} + +func signExtend(value int64, numBytes int64) int64 { + // we don't have to turn this to a uint64 first because numBytes < 8 so + // the sign bit will never pushed out of the int64 range + return (value << (64 - 8*numBytes)) >> (64 - 8*numBytes) +} + +func Require(runtime *goja.Runtime, module *goja.Object) { + b := &Buffer{r: runtime} + uint8Array := runtime.Get("Uint8Array") + if c, ok := goja.AssertConstructor(uint8Array); ok { + b.uint8ArrayCtor = c + } else { + panic(runtime.NewTypeError("Uint8Array is not a constructor")) + } + uint8ArrayObj := uint8Array.ToObject(runtime) + + ctor := runtime.ToValue(b.ctor).ToObject(runtime) + ctor.SetPrototype(uint8ArrayObj) + ctor.DefineDataPropertySymbol(symApi, runtime.ToValue(b), goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_FALSE) + b.bufferCtorObj = ctor + b.uint8ArrayCtorObj = uint8ArrayObj + + proto := runtime.NewObject() + proto.SetPrototype(uint8ArrayObj.Get("prototype").ToObject(runtime)) + proto.DefineDataProperty("constructor", ctor, goja.FLAG_TRUE, goja.FLAG_TRUE, goja.FLAG_FALSE) + proto.Set("equals", b.proto_equals) + proto.Set("toString", b.proto_toString) + proto.Set("readBigInt64BE", b.readBigInt64BE) + proto.Set("readBigInt64LE", b.readBigInt64LE) + proto.Set("readBigUInt64BE", b.readBigUInt64BE) + // aliases for readBigUInt64BE + proto.Set("readBigUint64BE", b.readBigUInt64BE) + + proto.Set("readBigUInt64LE", b.readBigUInt64LE) + // aliases for readBigUInt64LE + proto.Set("readBigUint64LE", b.readBigUInt64LE) + + proto.Set("readDoubleBE", b.readDoubleBE) + proto.Set("readDoubleLE", b.readDoubleLE) + proto.Set("readFloatBE", b.readFloatBE) + proto.Set("readFloatLE", b.readFloatLE) + proto.Set("readInt8", b.readInt8) + proto.Set("readInt16BE", b.readInt16BE) + proto.Set("readInt16LE", b.readInt16LE) + proto.Set("readInt32BE", b.readInt32BE) + proto.Set("readInt32LE", b.readInt32LE) + proto.Set("readIntBE", b.readIntBE) + proto.Set("readIntLE", b.readIntLE) + proto.Set("readUInt8", b.readUInt8) + // aliases for readUInt8 + proto.Set("readUint8", b.readUInt8) + + proto.Set("readUInt16BE", b.readUInt16BE) + // aliases for readUInt16BE + proto.Set("readUint16BE", b.readUInt16BE) + + proto.Set("readUInt16LE", b.readUInt16LE) + // aliases for readUInt16LE + proto.Set("readUint16LE", b.readUInt16LE) + + proto.Set("readUInt32BE", b.readUInt32BE) + // aliases for readUInt32BE + proto.Set("readUint32BE", b.readUInt32BE) + + proto.Set("readUInt32LE", b.readUInt32LE) + // aliases for readUInt32LE + proto.Set("readUint32LE", b.readUInt32LE) + + proto.Set("readUIntBE", b.readUIntBE) + // aliases for readUIntBE + proto.Set("readUintBE", b.readUIntBE) + + proto.Set("readUIntLE", b.readUIntLE) + // aliases for readUIntLE + proto.Set("readUintLE", b.readUIntLE) + proto.Set("write", b.write) + proto.Set("writeBigInt64BE", b.writeBigInt64BE) + proto.Set("writeBigInt64LE", b.writeBigInt64LE) + proto.Set("writeBigUInt64BE", b.writeBigUInt64BE) + // aliases for writeBigUInt64BE + proto.Set("writeBigUint64BE", b.writeBigUInt64BE) + + proto.Set("writeBigUInt64LE", b.writeBigUInt64LE) + // aliases for writeBigUInt64LE + proto.Set("writeBigUint64LE", b.writeBigUInt64LE) + + proto.Set("writeDoubleBE", b.writeDoubleBE) + proto.Set("writeDoubleLE", b.writeDoubleLE) + proto.Set("writeFloatBE", b.writeFloatBE) + proto.Set("writeFloatLE", b.writeFloatLE) + proto.Set("writeInt8", b.writeInt8) + proto.Set("writeInt16BE", b.writeInt16BE) + proto.Set("writeInt16LE", b.writeInt16LE) + proto.Set("writeInt32BE", b.writeInt32BE) + proto.Set("writeInt32LE", b.writeInt32LE) + proto.Set("writeIntBE", b.writeIntBE) + proto.Set("writeIntLE", b.writeIntLE) + proto.Set("writeUInt8", b.writeUInt8) + // aliases for writeUInt8 + proto.Set("writeUint8", b.writeUInt8) + + proto.Set("writeUInt16BE", b.writeUInt16BE) + // aliases for writeUInt16BE + proto.Set("writeUint16BE", b.writeUInt16BE) + + proto.Set("writeUInt16LE", b.writeUInt16LE) + // aliases for writeUInt16LE + proto.Set("writeUint16LE", b.writeUInt16LE) + + proto.Set("writeUInt32BE", b.writeUInt32BE) + // aliases for writeUInt32BE + proto.Set("writeUint32BE", b.writeUInt32BE) + + proto.Set("writeUInt32LE", b.writeUInt32LE) + // aliases for writeUInt32LE + proto.Set("writeUint32LE", b.writeUInt32LE) + + proto.Set("writeUIntBE", b.writeUIntBE) + // aliases for writeUIntBE + proto.Set("writeUintBE", b.writeUIntBE) + + proto.Set("writeUIntLE", b.writeUIntLE) + // aliases for writeUIntLE + proto.Set("writeUintLE", b.writeUIntLE) + + ctor.Set("prototype", proto) + ctor.Set("poolSize", 8192) + ctor.Set("from", b.from) + ctor.Set("alloc", b.alloc) + + exports := module.Get("exports").(*goja.Object) + exports.Set("Buffer", ctor) +} + +func init() { + require.RegisterCoreModule(ModuleName, Require) +} diff --git a/buffer/buffer_test.go b/buffer/buffer_test.go new file mode 100644 index 0000000..0450db3 --- /dev/null +++ b/buffer/buffer_test.go @@ -0,0 +1,2430 @@ +package buffer + +import ( + _ "embed" + "fmt" + "strings" + "testing" + + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/require" +) + +func TestBufferFrom(t *testing.T) { + vm := goja.New() + new(require.Registry).Enable(vm) + + _, err := vm.RunString(` + const Buffer = require("node:buffer").Buffer; + + function checkBuffer(buf) { + if (!(buf instanceof Buffer)) { + throw new Error("instanceof Buffer"); + } + + if (!(buf instanceof Uint8Array)) { + throw new Error("instanceof Uint8Array"); + } + } + + checkBuffer(Buffer.from(new ArrayBuffer(16))); + checkBuffer(Buffer.from(new Uint16Array(8))); + + { + const b = Buffer.from("\xff\xfe\xfd"); + const h = b.toString("hex") + if (h !== "c3bfc3bec3bd") { + throw new Error(h); + } + } + + { + const b = Buffer.from("0102fffdXXX", "hex"); + checkBuffer(b); + if (b.toString("hex") !== "0102fffd") { + throw new Error(b.toString("hex")); + } + } + + { + const b = Buffer.from('1ag123', 'hex'); + if (b.length !== 1 || b[0] !== 0x1a) { + throw new Error(b); + } + } + + { + const b = Buffer.from('1a7', 'hex'); + if (b.length !== 1 || b[0] !== 0x1a) { + throw new Error(b); + } + } + + { + const b = Buffer.from("\uD801", "utf-8"); + if (b.length !== 3 || b[0] !== 0xef || b[1] !== 0xbf || b[2] !== 0xbd) { + throw new Error(b); + } + } + `) + + if err != nil { + t.Fatal(err) + } +} + +func TestFromBase64(t *testing.T) { + vm := goja.New() + new(require.Registry).Enable(vm) + + _, err := vm.RunString(` + const Buffer = require("node:buffer").Buffer; + + { + let b = Buffer.from("AAA_", "base64"); + if (b.length !== 3 || b[0] !== 0 || b[1] !== 0 || b[2] !== 0x3f) { + throw new Error(b.toString("hex")); + } + + let r = b.toString("base64"); + if (r !== "AAA/") { + throw new Error("to base64: " + r); + } + for (let i = 0; i < 20; i++) { + let s = "A".repeat(i) + "_" + "A".repeat(20-i); + let s1 = "A".repeat(i) + "/" + "A".repeat(20-i); + let b = Buffer.from(s, "base64"); + let b1 = Buffer.from(s1, "base64"); + if (!b.equals(b1)) { + throw new Error(s); + } + } + } + + { + let b = Buffer.from("SQ==???", "base64"); + if (b.length !== 1 || b[0] != 0x49) { + throw new Error(b.toString("hex")); + } + } + + { + let s = Buffer.from("AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow", "base64Url").toString("base64"); + if (s !== "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ+EstJQLr/T+1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow==") { + throw new Error(s); + } + } + `) + + if err != nil { + t.Fatal(err) + } +} + +func TestWrapBytes(t *testing.T) { + vm := goja.New() + new(require.Registry).Enable(vm) + b := []byte{1, 2, 3} + buffer := GetApi(vm) + vm.Set("b", buffer.WrapBytes(b)) + Enable(vm) + _, err := vm.RunString(` + if (typeof Buffer !== "function") { + throw new Error("Buffer is not a function: " + typeof Buffer); + } + if (!(b instanceof Buffer)) { + throw new Error("instanceof Buffer"); + } + if (b.toString("hex") !== "010203") { + throw new Error(b); + } + `) + + if err != nil { + t.Fatal(err) + } +} + +func TestBuffer_alloc(t *testing.T) { + vm := goja.New() + new(require.Registry).Enable(vm) + + _, err := vm.RunString(` + const Buffer = require("node:buffer").Buffer; + + { + const b = Buffer.alloc(2, "abc"); + if (b.toString() !== "ab") { + throw new Error(b); + } + } + + { + const b = Buffer.alloc(16, "abc"); + if (b.toString() !== "abcabcabcabcabca") { + throw new Error(b); + } + } + + { + const fill = { + valueOf() { + return 0xac; + } + } + const b = Buffer.alloc(8, fill); + if (b.toString("hex") !== "acacacacacacacac") { + throw new Error(b); + } + } + + { + const fill = { + valueOf() { + return Infinity; + } + } + const b = Buffer.alloc(2, fill); + if (b.toString("hex") !== "0000") { + throw new Error(b); + } + } + + { + const fill = { + valueOf() { + return "ac"; + } + } + const b = Buffer.alloc(2, fill); + if (b.toString("hex") !== "0000") { + throw new Error(b); + } + } + + { + const b = Buffer.alloc(2, -257.4); + if (b.toString("hex") !== "ffff") { + throw new Error(b); + } + } + + { + const b = Buffer.alloc(2, Infinity); + if (b.toString("hex") !== "0000") { + throw new Error("Infinity: " + b.toString("hex")); + } + } + + { + const b = Buffer.alloc(2, null); + if (b.toString("hex") !== "0000") { + throw new Error("Infinity: " + b.toString("hex")); + } + } + + `) + + if err != nil { + t.Fatal(err) + } +} + +//go:embed testdata/assertions.js +var assertionsSource string + +type testCase struct { + name string + script string + expectedErr string +} + +func runTestCases(t *testing.T, tcs []testCase) { + vm := goja.New() + new(require.Registry).Enable(vm) + _, err := vm.RunScript("testdata/assertions.js", assertionsSource) + if err != nil { + t.Fatal(err) + } + _, err = vm.RunString(`const Buffer = require("node:buffer").Buffer;`) + if err != nil { + t.Fatal(err) + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + template := ` + { + %s + } + ` + _, err := vm.RunString(fmt.Sprintf(template, tc.script)) + if tc.expectedErr != "" { + if err == nil { + t.Errorf("expected error %q, but got none", tc.expectedErr) + } else { + if !strings.HasPrefix(err.Error(), tc.expectedErr) { + t.Errorf("expected error %q, got %q", tc.expectedErr, err.Error()) + } + } + } else { + if err != nil { + t.Errorf("expected no error, but got %q", err.Error()) + } + } + }) + } +} + +func TestBuffer_readBigInt64BE(t *testing.T) { + tcs := []testCase{ + { + name: "with zero offset", + script: ` + const b = Buffer.from([0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff]); + if (b.readBigInt64BE(0) !== BigInt(4294967295)) { + throw new Error(b); + } + `, + }, + { + name: "with no/default offset", + script: ` + const b = Buffer.from([0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff]); + if (b.readBigInt64BE() !== BigInt(4294967295)) { + throw new Error(b); + } + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readBigInt64LE(t *testing.T) { + tcs := []testCase{ + { + name: "with zero offset", + script: ` + const b = Buffer.from([0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff]); + if (b.readBigInt64LE(0) !== BigInt(-4294967296)) { + throw new Error(b); + } + `, + }, + { + name: "with out of range offset", + script: ` + const b = Buffer.from([0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff]); + // this should error + b.readBigInt64LE(1); + throw new Error("should not get here"); + `, + expectedErr: `RangeError [ERR_OUT_OF_RANGE]: The value of "offset" 1 is out of range`, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readBigUInt64BE(t *testing.T) { + tcs := []testCase{ + { + name: "with zero offset", + script: ` + const b = Buffer.from([0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff]); + if (b.readBigUInt64BE(0) !== BigInt(4294967295)) { + throw new Error(b); + } + `, + }, + { + name: "with no/default offset", + script: ` + const b = Buffer.from([0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff]); + if (b.readBigUInt64BE() !== BigInt(4294967295)) { + throw new Error(b); + } + `, + }, + { + name: "use alias", + script: ` + const b = Buffer.from([0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff]); + if (b.readBigUint64BE() !== BigInt(4294967295)) { + throw new Error(b); + } + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readBigUInt64LE(t *testing.T) { + tcs := []testCase{ + { + name: "with zero offset", + script: ` + const b = Buffer.from([0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff]); + if (b.readBigUInt64LE(0) !== BigInt(18446744069414584320)) { + throw new Error(b); + } + `, + }, + { + name: "with out of range offset", + script: ` + const b = Buffer.from([0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff]); + // this should error + b.readBigUInt64LE(1); + throw new Error("should not get here"); + `, + expectedErr: `RangeError [ERR_OUT_OF_RANGE]: The value of "offset" 1 is out of range`, + }, + { + name: "use alias", + script: ` + const b = Buffer.from([0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff]); + if (b.readBigUint64LE(0) !== BigInt(18446744069414584320)) { + throw new Error(b); + } + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readDoubleBE(t *testing.T) { + tcs := []testCase{ + { + name: "with zero offset", + script: ` + const b = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]); + if (b.readDoubleBE(0) !== 8.20788039913184e-304) { + throw new Error(b); + } + `, + }, + { + name: "with no/default offset", + script: ` + const b = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]); + if (b.readDoubleBE() !== 8.20788039913184e-304) { + throw new Error(b); + } + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readDoubleLE(t *testing.T) { + tcs := []testCase{ + { + name: "with zero offset", + script: ` + const b = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]); + if (b.readDoubleLE(0) !== 5.447603722011605e-270) { + throw new Error(b); + } + `, + }, + { + name: "with out of range offset", + script: ` + const b = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]); + // this should error + b.readDoubleLE(1); + throw new Error("should not get here"); + `, + expectedErr: `RangeError [ERR_OUT_OF_RANGE]: The value of "offset" 1 is out of range`, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readFloatBE(t *testing.T) { + tcs := []testCase{ + { + name: "with zero offset", + script: ` + const b = Buffer.from([1, 2, 3, 4]); + if (b.readFloatBE(0) !== 2.387939260590663e-38) { + throw new Error(b); + } + `, + }, + { + name: "with no/default offset", + script: ` + const b = Buffer.from([1, 2, 3, 4]); + if (b.readFloatBE() !== 2.387939260590663e-38) { + throw new Error(b); + } + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readFloatLE(t *testing.T) { + tcs := []testCase{ + { + name: "with zero offset", + script: ` + const b = Buffer.from([1, 2, 3, 4]); + if (b.readFloatLE(0) !== 1.539989614439558e-36) { + throw new Error(b); + } + `, + }, + { + name: "with out of range offset", + script: ` + const b = Buffer.from([1, 2, 3, 4]); + // this should error + b.readFloatLE(1); + throw new Error("should not get here"); + `, + expectedErr: `RangeError [ERR_OUT_OF_RANGE]: The value of "offset" 1 is out of range`, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readInt8(t *testing.T) { + tcs := []testCase{ + { + name: "with zero offset", + script: ` + const b = Buffer.from([-1, 5]); + if (b.readInt8(0) !== -1) { + throw new Error(b); + } + `, + }, + { + name: "with last offset", + script: ` + const b = Buffer.from([-1, 5]); + if (b.readInt8(1) !== 5) { + throw new Error(b); + } + `, + }, + { + name: "with no/default offset", + script: ` + const b = Buffer.from([-1, 5]); + if (b.readInt8() !== -1) { + throw new Error(b); + } + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readInt16BE(t *testing.T) { + tcs := []testCase{ + { + name: "with zero offset", + script: ` + const b = Buffer.from([0, 5]); + if (b.readInt16BE(0) !== 5) { + throw new Error(b); + } + `, + }, + { + name: "with out of range offset", + script: ` + const b = Buffer.from([0xA, 0x5]); + // this should error + b.readInt16BE(1); + throw new Error("should not get here"); + `, + expectedErr: `RangeError [ERR_OUT_OF_RANGE]: The value of "offset" 1 is out of range`, + }, + { + name: "with no/default offset", + script: ` + const b = Buffer.from([0, 5]); + if (b.readInt16BE() !== 5) { + throw new Error(b); + } + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readInt16LE(t *testing.T) { + tcs := []testCase{ + { + name: "with zero offset", + script: ` + const b = Buffer.from([0, 5]); + if (b.readInt16LE(0) !== 1280) { + throw new Error(b); + } + `, + }, + { + name: "with out of range offset", + script: ` + const b = Buffer.from([0, 5]); + // this should error + b.readInt16LE(1); + throw new Error("should not get here"); + `, + expectedErr: `RangeError [ERR_OUT_OF_RANGE]: The value of "offset" 1 is out of range`, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readInt32BE(t *testing.T) { + tcs := []testCase{ + { + name: "with zero offset", + script: ` + const b = Buffer.from([0, 0, 0, 5]); + if (b.readInt32BE(0) !== 5) { + throw new Error(b); + } + `, + }, + { + name: "with out of range offset", + script: ` + const b = Buffer.from([0, 0, 0, 5]); + // this should error + b.readInt32BE(1); + throw new Error("should not get here"); + `, + expectedErr: `RangeError [ERR_OUT_OF_RANGE]: The value of "offset" 1 is out of range`, + }, + { + name: "with no/default offset", + script: ` + const b = Buffer.from([0, 0, 0, 5]); + if (b.readInt32BE() !== 5) { + throw new Error(b); + } + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readInt32LE(t *testing.T) { + tcs := []testCase{ + { + name: "with zero offset", + script: ` + const b = Buffer.from([0, 0, 0, 5]); + if (b.readInt32LE(0) !== 83886080) { + throw new Error(b); + } + `, + }, + { + name: "with out of range offset", + script: ` + const b = Buffer.from([0, 0, 0, 5]); + // this should error + b.readInt32LE(1); + throw new Error("should not get here"); + `, + expectedErr: `RangeError [ERR_OUT_OF_RANGE]: The value of "offset" 1 is out of range`, + }, + { + name: "with no/default offset", + script: ` + const b = Buffer.from([0, 0, 0, 5]); + if (b.readInt32LE() !== 83886080) { + throw new Error(b); + } + `, + }, + } + + runTestCases(t, tcs) +} + +func TestName(t *testing.T) { + +} + +func TestBuffer_readIntBE(t *testing.T) { + tcs := []testCase{ + { + name: "6 byte positive integer", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + if (b.readIntBE(0, 6) !== 20015998341291) { + throw new Error(b); + } + `, + }, + { + name: "1 byte negative integer", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + if (b.readIntBE(4, 1) !== -112) { + throw new Error(b); + } + `, + }, + { + name: "with no parameters", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + // this should error + b.readIntBE(); + throw new Error("should not get here"); + `, + expectedErr: `TypeError [ERR_INVALID_ARG_TYPE]: The "offset" argument is required`, + }, + { + name: "type mismatch for offset", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + // this should error + b.readIntBE("1"); + throw new Error("should not get here"); + `, + expectedErr: `TypeError [ERR_INVALID_ARG_TYPE]: The "offset" argument must be of type number`, + }, + { + name: "with no byteLength parameter", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + // this should error + b.readIntBE(0); + throw new Error("should not get here"); + `, + expectedErr: `TypeError [ERR_INVALID_ARG_TYPE]: The "byteLength" argument is required`, + }, + { + name: "byteLength less than 1", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + // this should error + b.readIntBE(0,0); + throw new Error("should not get here"); + `, + expectedErr: `RangeError [ERR_OUT_OF_RANGE]: The value of "byteLength" 0 is out of range`, + }, + { + name: "byteLength greater than 7", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + // this should error + b.readIntBE(0,7); + throw new Error("should not get here"); + `, + expectedErr: `RangeError [ERR_OUT_OF_RANGE]: The value of "byteLength" 7 is out of range`, + }, + { + name: "offset plus byteLength out of range", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + // this should error + b.readIntBE(4,3); + throw new Error("should not get here"); + `, + expectedErr: `RangeError [ERR_OUT_OF_RANGE]: The value of "offset" 4 is out of range`, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readIntLE(t *testing.T) { + tcs := []testCase{ + { + name: "6 byte negative integer", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + if (b.readIntLE(0, 6) !== -92837994154990) { + throw new Error(b); + } + `, + }, + { + name: "1 byte positive integer", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + if (b.readIntLE(0, 1) !== 18) { + throw new Error(b); + } + `, + }, + { + name: "with no parameters", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + // this should error + b.readIntLE(); + throw new Error("should not get here"); + `, + expectedErr: `TypeError [ERR_INVALID_ARG_TYPE]: The "offset" argument is required`, + }, + { + name: "with no byteLength parameter", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + // this should error + b.readIntLE(0); + throw new Error("should not get here"); + `, + expectedErr: `TypeError [ERR_INVALID_ARG_TYPE]: The "byteLength" argument is required`, + }, + { + name: "offset plus byteLength out of range", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + // this should error + b.readIntLE(4,3); + throw new Error("should not get here"); + `, + expectedErr: `RangeError [ERR_OUT_OF_RANGE]: The value of "offset" 4 is out of range`, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readUInt8(t *testing.T) { + tcs := []testCase{ + { + name: "with zero offset", + script: ` + const b = Buffer.from([1, -2]); + if (b.readUInt8(0) !== 1) { + throw new Error(b); + } + `, + }, + { + name: "with last offset", + script: ` + const b = Buffer.from([1, -2]); + if (b.readUInt8(1) !== 254) { + throw new Error(b); + } + `, + }, + { + name: "with no/default offset", + script: ` + const b = Buffer.from([1, -2]); + if (b.readUInt8() !== 1) { + throw new Error(b); + } + `, + }, + { + name: "use alias", + script: ` + const b = Buffer.from([1, -2]); + if (b.readUint8() !== 1) { + throw new Error(b); + } + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readUInt16BE(t *testing.T) { + tcs := []testCase{ + { + name: "with zero offset", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56]); + if (b.readUInt16BE(0).toString(16) !== "1234") { + throw new Error(b); + } + `, + }, + { + name: "with last offset", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56]); + if (b.readUInt16BE(1).toString(16) !== "3456") { + throw new Error(b); + } + `, + }, + { + name: "with no/default offset", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56]); + if (b.readUInt16BE().toString(16) !== "1234") { + throw new Error(b); + } + `, + }, + { + name: "use alias", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56]); + if (b.readUint16BE().toString(16) !== "1234") { + throw new Error(b); + } + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readUInt16LE(t *testing.T) { + tcs := []testCase{ + { + name: "with zero offset", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56]); + if (b.readUInt16LE(0).toString(16) !== "3412") { + throw new Error(b); + } + `, + }, + { + name: "with last offset", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56]); + if (b.readUInt16LE(1).toString(16) !== "5634") { + throw new Error(b); + } + `, + }, + { + name: "out of range offset", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56]); + // this should error + b.readUInt16LE(2); + throw new Error("should not get here"); + `, + expectedErr: `RangeError [ERR_OUT_OF_RANGE]: The value of "offset" 2 is out of range`, + }, + { + name: "use alias", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56]); + if (b.readUint16LE(1).toString(16) !== "5634") { + throw new Error(b); + } + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readUInt32BE(t *testing.T) { + tcs := []testCase{ + { + name: "with zero offset", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78]); + if (b.readUInt32BE(0).toString(16) !== "12345678") { + throw new Error(b); + } + `, + }, + { + name: "with no/default offset", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78]); + if (b.readUInt32BE().toString(16) !== "12345678") { + throw new Error(b); + } + `, + }, + { + name: "use alias", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78]); + if (b.readUint32BE(0).toString(16) !== "12345678") { + throw new Error(b); + } + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readUInt32LE(t *testing.T) { + tcs := []testCase{ + { + name: "with zero offset", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78]); + if (b.readUInt32LE(0).toString(16) !== "78563412") { + throw new Error(b); + } + `, + }, + { + name: "with no/default offset", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78]); + if (b.readUInt32LE().toString(16) !== "78563412") { + throw new Error(b); + } + `, + }, + { + name: "with string offset", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78]); + // this should error + b.readUInt32LE("foo"); + throw new Error("should not get here");// this should error + `, + expectedErr: `TypeError [ERR_INVALID_ARG_TYPE]: The "offset" argument must be of type number`, + }, + { + name: "with negative offset", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78]); + // this should error + b.readUInt32LE(-1); + throw new Error("should not get here");// this should error + `, + expectedErr: `RangeError [ERR_OUT_OF_RANGE]: The value of "offset" -1 is out of range`, + }, + { + name: "use alias", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78]); + if (b.readUint32LE(0).toString(16) !== "78563412") { + throw new Error(b); + } + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readUIntBE(t *testing.T) { + tcs := []testCase{ + { + name: "6 byte integer", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + if (b.readUIntBE(0, 6) !== 20015998341291) { + throw new Error(b); + } + `, + }, + { + name: "1 byte integer", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + if (b.readUIntBE(1, 1) !== 52) { + throw new Error(b); + } + `, + }, + { + name: "with no parameters", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + // this should error + b.readUIntBE(); + throw new Error("should not get here"); + `, + expectedErr: `TypeError [ERR_INVALID_ARG_TYPE]: The "offset" argument is required`, + }, + { + name: "with no byteLength parameter", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + // this should error + b.readUIntBE(0); + throw new Error("should not get here"); + `, + expectedErr: `TypeError [ERR_INVALID_ARG_TYPE]: The "byteLength" argument is required`, + }, + { + name: "offset plus byteLength out of range", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + // this should error + b.readUIntBE(4,3); + throw new Error("should not get here"); + `, + expectedErr: `RangeError [ERR_OUT_OF_RANGE]: The value of "offset" 4 is out of range`, + }, + { + name: "use alias", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + if (b.readUintBE(1, 1) !== 52) { + throw new Error(b); + } + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_readUIntLE(t *testing.T) { + tcs := []testCase{ + { + name: "6 byte integer", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + if (b.readUIntLE(0, 6) !== 188636982555666) { + throw new Error(b); + } + `, + }, + { + name: "1 byte integer", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + if (b.readUIntLE(1, 1) !== 52) { + throw new Error(b); + } + `, + }, + { + name: "with no parameters", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + // this should error + b.readUIntLE(); + throw new Error("should not get here"); + `, + expectedErr: `TypeError [ERR_INVALID_ARG_TYPE]: The "offset" argument is required`, + }, + { + name: "with no byteLength parameter", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + // this should error + b.readUIntLE(0); + throw new Error("should not get here"); + `, + expectedErr: `TypeError [ERR_INVALID_ARG_TYPE]: The "byteLength" argument is required`, + }, + { + name: "offset plus byteLength out of range", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + // this should error + b.readUIntLE(4,3); + throw new Error("should not get here"); + `, + expectedErr: `RangeError [ERR_OUT_OF_RANGE]: The value of "offset" 4 is out of range`, + }, + { + name: "use alias", + script: ` + const b = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + if (b.readUintLE(1, 1) !== 52) { + throw new Error(b); + } + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_toString(t *testing.T) { + tcs := []testCase{ + { + name: "with no parameters", + script: ` + const buf = Buffer.alloc(5); + buf.write('hello'); + + if (buf.toString() !== 'hello') { + throw new Error('should return "hello"'); + } + `, + }, + { + name: "utf8 encoding", + script: ` + const buf = Buffer.from([0x7E]); + if (buf.toString('utf8') !== '~') { + throw new Error('should return "~"'); + } + `, + }, + { + name: "with valid start and valid end", + script: ` + const buf = Buffer.alloc(10); + buf.write('hello world'); + + if (buf.toString('utf8', 6, 10) !== 'worl') { + throw new Error('should return "worl"'); + } + `, + }, + { + name: "with start=0 and end=0", + script: ` + const buf = Buffer.alloc(10); + buf.write('hello world'); + + if (buf.toString('utf8', 0, 0) !== '') { + throw new Error('should return empty'); + } + `, + }, + { + name: "with start > end", + script: ` + const buf = Buffer.alloc(10); + buf.write('hello world'); + + if (buf.toString('utf8', 3, 2) !== '') { + throw new Error('should return empty'); + } + `, + }, + { + name: "with start < 0", + script: ` + const buf = Buffer.alloc(10); + buf.write('hello world'); + + if (buf.toString('utf8', -1, 2) !== 'he') { + throw new Error('should return "he"'); + } + `, + }, + { + name: "with start > buffer length", + script: ` + const buf = Buffer.alloc(10); + buf.write('hello world'); + + if (buf.toString('utf8', 100, 2) !== '') { + throw new Error('should return empty'); + } + `, + }, + { + name: "with end > buffer length", + script: ` + const buf = Buffer.alloc(10); + buf.write('hello world'); + + if (buf.toString('utf8', 1, 100) !== 'ello worl') { + throw new Error('should return "ello worl"'); + } + `, + }, + { + name: "with start == buffer length", + script: ` + const buf = Buffer.alloc(10); + buf.write('hello world'); + + if (buf.toString('utf8', 10, 2) !== '') { + throw new Error('should return empty'); + } + `, + }, + { + name: "with non-numeric start", + script: ` + const buf = Buffer.alloc(10); + buf.write('hello world'); + + if (buf.toString('utf8', {}, 2) !== 'he') { + throw new Error('should return "he"'); + } + `, + }, + { + name: "with float start", + script: ` + const buf = Buffer.alloc(10); + buf.write('hello world'); + + if (buf.toString('utf8', 3.5, 10) !== 'lo worl') { + throw new Error('should return "lo worl"'); + } + `, + }, + { + name: "with float end", + script: ` + const buf = Buffer.alloc(10); + buf.write('hello world'); + + if (buf.toString('utf8', 0, 4.9) !== 'hell') { + throw new Error('should return "hell"'); + } + `, + }, + { + name: "with multi-byte character", + script: ` + const buf = Buffer.from([0xE2, 0x82, 0xAC]); + + if (buf.toString('utf8') !== '€') { + throw new Error('should return "€"'); + } + `, + }, + { + name: "with partitial multi-byte character", + script: ` + const buf = Buffer.from([0xE2, 0x82, 0xAC, 0xE2, 0x82, 0xAC]); + + if (buf.toString('utf8',0, 4) !== '€�') { + throw new Error('should return "€�"'); + } + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_write(t *testing.T) { + tcs := []testCase{ + { + name: "write string with defaults", + script: ` + const buf = Buffer.alloc(10); + const bytesWritten = buf.write('hello'); + + if (bytesWritten !== 5) { + throw new Error('bytesWritten should be 5'); + + } else if (buf.toString('utf8', 0, 5) !== 'hello') { + throw new Error('buffer content should be "hello"'); + + } else if (buf.toString('utf8', 5, 10) !== '\0\0\0\0\0') { + throw new Error('remaining buffer should be zeros'); + } + `, + }, + { + name: "write at offset", + script: ` + const buf = Buffer.alloc(10); + const bytesWritten = buf.write('world', 5); + + if (bytesWritten !== 5) { + throw new Error('bytesWritten should be 5'); + + } else if (buf.toString('utf8', 5, 10) !== 'world') { + throw new Error('buffer content should be "world"'); + + } else if (buf.toString('utf8', 0, 5) !== '\0\0\0\0\0') { + throw new Error('first 5 bytes should be zeros'); + } + `, + }, + { + name: "write with offset and length", + script: ` + const buf = Buffer.alloc(10); + const bytesWritten = buf.write('hello world', 0, 5); + + if (bytesWritten !== 5) { + throw new Error('bytesWritten should be 5'); + + } else if (buf.toString('utf8', 0, 5) !== 'hello') { + throw new Error('buffer content should be "hello"'); + + } else if (buf.toString('utf8', 5, 10) !== '\0\0\0\0\0') { + throw new Error('remaining buffer should be zeros'); + } + `, + }, + { + name: "write at offset zero", + script: ` + const buf = Buffer.alloc(5); + const bytesWritten = buf.write('abc', 0); + + if (bytesWritten !== 3) { + throw new Error('bytesWritten should be 3'); + + } else if (buf.toString('utf8', 0, 3) !== 'abc') { + throw new Error('buffer content should be "abc"'); + } + `, + }, + { + name: "write at last offset", + script: ` + const buf = Buffer.alloc(5); + const bytesWritten = buf.write('a', 4); + + if (bytesWritten !== 1) { + throw new Error('bytesWritten should be 3'); + + } else if (buf[4] !== 'a'.charCodeAt(0)) { + throw new Error('buf[4] should be "a"'); + } + `, + }, + { + name: "write with length zero", + script: ` + const buf = Buffer.alloc(5); + const bytesWritten = buf.write('abc', 0, 0); + + if (bytesWritten !== 0) { + throw new Error('bytesWritten should be 0'); + + } else if (buf.toString('utf8', 0, 5) !== '\0\0\0\0\0') { + throw new Error('buffer should remain zeros'); + } + `, + }, + { + name: "write with length greater than string length", + script: ` + const buf = Buffer.alloc(5); + const bytesWritten = buf.write('abc', 0, 5); + + if (bytesWritten !== 3) { + throw new Error('bytesWritten should be 3'); + + } else if (buf.toString('utf8', 0, 3) !== 'abc') { + throw new Error('buffer content should be "abc"'); + } + `, + }, + { + name: "write with offset + length exceeding buffer length", + script: ` + const buf = Buffer.alloc(5); + const bytesWritten = buf.write('abcde', 3); + + if (bytesWritten !== 2) { + throw new Error('bytesWritten should be 2'); + + } else if (buf.toString('utf8', 3, 5) !== 'ab') { + throw new Error('buffer content at 3-5 should be "ab"'); + } + `, + }, + { + name: "invalid encoding", + script: ` + const buf = Buffer.alloc(5); + + // this should error + buf.write('a', 0, 1, 'invalid'); + throw new Error("should not get here"); + `, + expectedErr: `TypeError [ERR_UNKNOWN_ENCODING]: Unknown encoding: invalid`, + }, + { + name: "offset out of range", + script: ` + const buf = Buffer.alloc(5); + + // this should error + buf.write('abc', 10); + throw new Error("should not get here"); + `, + expectedErr: `RangeError [ERR_OUT_OF_RANGE]: The value of "offset" 10 is out of range`, + }, + { + name: "with no parameters", + script: ` + const buf = Buffer.alloc(5); + // this should error + buf.write(); + throw new Error("should not get here"); + `, + expectedErr: `TypeError [ERR_INVALID_ARG_TYPE]: The "string" argument is required`, + }, + { + name: "argument not string type", + script: ` + const buf = Buffer.alloc(5); + // this should error + buf.write(1); + throw new Error("should not get here"); + `, + expectedErr: `TypeError [ERR_INVALID_ARG_TYPE]: The "string" argument must be of type string`, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeBigInt64BE(t *testing.T) { + + tcs := []testCase{ + { + name: "write with default offset", + script: ` + const buf = Buffer.alloc(16); + assertBufferWriteRead(buf, 'writeBigInt64BE', 'readBigInt64BE', BigInt(123456789)); + `, + }, + { + name: "write negative number, zero offset", + script: ` + const buf = Buffer.alloc(16); + assertBufferWriteRead(buf, 'writeBigInt64BE', 'readBigInt64BE', BigInt(123456789), 0); + `, + }, + { + name: "write at non-zero offset", + script: ` + const buf = Buffer.alloc(16); + assertBufferWriteRead(buf, 'writeBigInt64BE', 'readBigInt64BE', BigInt(123456789), 4); + `, + }, + { + name: "write with max int64 value", + script: ` + const buf = Buffer.alloc(16); + assertBufferWriteRead(buf, 'writeBigInt64BE', 'readBigInt64BE', BigInt("9223372036854775807")); // 2^63 - 1 + `, + }, + { + name: "write with min int64 value", + script: ` + const buf = Buffer.alloc(16); + assertBufferWriteRead(buf, 'writeBigInt64BE', 'readBigInt64BE', BigInt("-9223372036854775808")); // -2^63 + `, + }, + { + name: "write with out of range offset", + script: ` + const buf = Buffer.alloc(8); + assert.throwsNodeErrorWithMessage(() => buf.writeBigInt64BE(BigInt(1), 1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 1 is out of range.'); + `, + }, + { + name: "write with negative offset", + script: ` + const buf = Buffer.alloc(16); + assert.throwsNodeErrorWithMessage(() => buf.writeBigInt64BE(BigInt(1), -1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" -1 is out of range.'); + `, + }, + { + name: "write without required value", + script: ` + const buf = Buffer.alloc(16); + assert.throwsNodeErrorWithMessage(() => buf.writeBigInt64BE(), TypeError, "ERR_INVALID_ARG_TYPE", 'The "value" argument is required.'); + `, + }, + { + name: "write with number instead of BigInt", + script: ` + const buf = Buffer.alloc(16); + assert.throwsNodeErrorWithMessage(() => buf.writeBigInt64BE(123456789, 0), TypeError, "ERR_INVALID_ARG_TYPE", 'The "value" argument must be of type BigInt.'); + `, + }, + { + name: "writing and then reading different sections of buffer", + script: ` + const buf = Buffer.alloc(24); + buf.writeBigInt64BE(BigInt(123)); + buf.writeBigInt64BE(BigInt(456), 8); + buf.writeBigInt64BE(BigInt(789), 16); + + assertValueRead(buf.readBigInt64BE(0), BigInt(123)); + assertValueRead(buf.readBigInt64BE(8), BigInt(456)); + assertValueRead(buf.readBigInt64BE(16), BigInt(789)); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeBigInt64LE(t *testing.T) { + tcs := []testCase{ + { + name: "write with default offset", + script: ` + const buf = Buffer.alloc(16); + assertBufferWriteRead(buf, 'writeBigInt64LE', 'readBigInt64LE', BigInt(123456789)); + `, + }, + { + name: "write negative number, zero offset", + script: ` + const buf = Buffer.alloc(16); + assertBufferWriteRead(buf, 'writeBigInt64LE', 'readBigInt64LE', BigInt(-123456789), 0); + `, + }, + { + name: "write at non-zero offset", + script: ` + const buf = Buffer.alloc(16); + assertBufferWriteRead(buf, 'writeBigInt64LE', 'readBigInt64LE', BigInt(123456789), 4); + `, + }, + { + name: "write with out of range offset", + script: ` + const buf = Buffer.alloc(8); + assert.throwsNodeErrorWithMessage(() => buf.writeBigInt64LE(BigInt(1), 1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 1 is out of range.'); + `, + }, + { + name: "writing and then reading different sections of buffer", + script: ` + const buf = Buffer.alloc(24); + buf.writeBigInt64LE(BigInt(123)); + buf.writeBigInt64LE(BigInt(456), 8); + buf.writeBigInt64LE(BigInt(789), 16); + + assertValueRead(buf.readBigInt64LE(0), BigInt(123)); + assertValueRead(buf.readBigInt64LE(8), BigInt(456)); + assertValueRead(buf.readBigInt64LE(16), BigInt(789)); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeBigUInt64BE(t *testing.T) { + tcs := []testCase{ + { + name: "write with default offset", + script: ` + const buf = Buffer.alloc(16); + assertBufferWriteRead(buf, 'writeBigUInt64BE', 'readBigUInt64BE', BigInt(123456789)); + `, + }, + { + name: "read/write using alias", + script: ` + const buf = Buffer.alloc(16); + assertBufferWriteRead(buf, 'writeBigUint64BE', 'readBigUint64BE', BigInt(123456789)); + `, + }, + { + name: "write at non-zero offset", + script: ` + const buf = Buffer.alloc(16); + assertBufferWriteRead(buf, 'writeBigUInt64BE', 'readBigUInt64BE', BigInt(123456789), 4); + `, + }, + { + name: "write with out of range offset", + script: ` + const buf = Buffer.alloc(8); + assert.throwsNodeErrorWithMessage(() => buf.writeBigUInt64BE(BigInt(1), 1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 1 is out of range.'); + `, + }, + { + name: "writing and then reading different sections of buffer", + script: ` + const buf = Buffer.alloc(24); + buf.writeBigUInt64BE(BigInt(123)); + buf.writeBigUInt64BE(BigInt(456), 8); + buf.writeBigUInt64BE(BigInt(789), 16); + + assertValueRead(buf.readBigUInt64BE(0), BigInt(123)); + assertValueRead(buf.readBigUInt64BE(8), BigInt(456)); + assertValueRead(buf.readBigUInt64BE(16), BigInt(789)); + `, + }, + { + name: "write large unsigned value", + script: ` + const buf = Buffer.alloc(16); + assertBufferWriteRead(buf, 'writeBigUInt64BE', 'readBigUInt64BE', BigInt("9007199254740991")); // MAX_SAFE_INTEGER + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeBigUInt64LE(t *testing.T) { + tcs := []testCase{ + { + name: "write with default offset", + script: ` + const buf = Buffer.alloc(16); + assertBufferWriteRead(buf, 'writeBigUInt64LE', 'readBigUInt64LE', BigInt(123456789)); + `, + }, + { + name: "read/write using alias", + script: ` + const buf = Buffer.alloc(16); + assertBufferWriteRead(buf, 'writeBigUint64LE', 'readBigUint64LE', BigInt(123456789)); + `, + }, + { + name: "write at non-zero offset", + script: ` + const buf = Buffer.alloc(16); + assertBufferWriteRead(buf, 'writeBigUint64LE', 'readBigUint64LE', BigInt(123456789), 4); + `, + }, + { + name: "write with out of range offset", + script: ` + const buf = Buffer.alloc(8); + assert.throwsNodeErrorWithMessage(() => buf.writeBigUInt64LE(BigInt(1), 1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 1 is out of range.'); + `, + }, + { + name: "writing and then reading different sections of buffer", + script: ` + const buf = Buffer.alloc(24); + buf.writeBigUInt64LE(BigInt(123)); + buf.writeBigUInt64LE(BigInt(456), 8); + buf.writeBigUInt64LE(BigInt(789), 16); + + assertValueRead(buf.readBigUInt64LE(0), BigInt(123)); + assertValueRead(buf.readBigUInt64LE(8), BigInt(456)); + assertValueRead(buf.readBigUInt64LE(16), BigInt(789)); + `, + }, + { + name: "write max uint64 value", + script: ` + const buf = Buffer.alloc(16); + assertBufferWriteRead(buf, 'writeBigUint64LE', 'readBigUint64LE', BigInt("18446744073709551615")); // 2^64 - 1 + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeDoubleBE(t *testing.T) { + tcs := []testCase{ + { + name: "write with out of range offset", + script: ` + const buf = Buffer.alloc(8); + assert.throwsNodeErrorWithMessage(() => buf.writeDoubleBE(123.456, 1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 1 is out of range.'); + `, + }, + { + name: "writing and then reading different sections of buffer", + script: ` + const buf = Buffer.alloc(24); + buf.writeDoubleBE(123.1); // default offset of zero + buf.writeDoubleBE(456.4, 8); + buf.writeDoubleBE(789.7, 16); + + assertValueRead(buf.readDoubleBE(0), 123.1); + assertValueRead(buf.readDoubleBE(8), 456.4); + assertValueRead(buf.readDoubleBE(16), 789.7); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeDoubleLE(t *testing.T) { + tcs := []testCase{ + { + name: "write with out of range offset", + script: ` + const buf = Buffer.alloc(8); + assert.throwsNodeErrorWithMessage(() => buf.writeDoubleLE(123.456, 1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 1 is out of range.'); + `, + }, + { + name: "writing and then reading different sections of buffer", + script: ` + const buf = Buffer.alloc(24); + buf.writeDoubleLE(123.1); // default offset of zero + buf.writeDoubleLE(456.4, 8); + buf.writeDoubleLE(789.7, 16); + + assertValueRead(buf.readDoubleLE(0), 123.1); + assertValueRead(buf.readDoubleLE(8), 456.4); + assertValueRead(buf.readDoubleLE(16), 789.7); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeFloatBE(t *testing.T) { + tcs := []testCase{ + { + name: "write with out of range offset", + script: ` + const buf = Buffer.alloc(4); + assert.throwsNodeErrorWithMessage(() => buf.writeFloatBE(123.456, 1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 1 is out of range.'); + `, + }, + { + name: "number exceeds ranges", + script: ` + const buf = Buffer.alloc(4); + // above the max + assert.throwsNodeErrorWithMessage(() => buf.writeFloatBE(3.5e+38, 0, 6), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 3.5e+38 is out of range.'); + // below the min + assert.throwsNodeErrorWithMessage(() => buf.writeFloatBE(-3.5e+38, 0, 6), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -3.5e+38 is out of range.'); + `, + }, + { + name: "writing and then reading different sections of buffer", + script: ` + const buf = Buffer.alloc(12); + buf.writeFloatBE(123.1); // default offset of zero + buf.writeFloatBE(456.4, 4); + buf.writeFloatBE(789.7, 8); + + assertValueRead(buf.readFloatBE(0), 123.1); + assertValueRead(buf.readFloatBE(4), 456.4); + assertValueRead(buf.readFloatBE(8), 789.7); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeFloatLE(t *testing.T) { + tcs := []testCase{ + { + name: "write with out of range offset", + script: ` + const buf = Buffer.alloc(4); + assert.throwsNodeErrorWithMessage(() => buf.writeFloatLE(123.456, 1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 1 is out of range.'); + `, + }, + { + name: "number exceeds ranges", + script: ` + const buf = Buffer.alloc(4); + // above the max + assert.throwsNodeErrorWithMessage(() => buf.writeFloatLE(3.5e+38, 0, 6), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 3.5e+38 is out of range.'); + // below the min + assert.throwsNodeErrorWithMessage(() => buf.writeFloatLE(-3.5e+38, 0, 6), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -3.5e+38 is out of range.'); + `, + }, + { + name: "writing and then reading different sections of buffer", + script: ` + const buf = Buffer.alloc(12); + buf.writeFloatLE(123.1); // default offset of zero + buf.writeFloatLE(456.4, 4); + buf.writeFloatLE(789.7, 8); + + assertValueRead(buf.readFloatLE(0), 123.1); + assertValueRead(buf.readFloatLE(4), 456.4); + assertValueRead(buf.readFloatLE(8), 789.7); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeInt8(t *testing.T) { + tcs := []testCase{ + { + name: "write with out of range offset", + script: ` + const buf = Buffer.alloc(2); + assert.throwsNodeErrorWithMessage(() => buf.writeInt8(2, 2), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 2 is out of range.'); + `, + }, + { + name: "number exceeds ranges", + script: ` + const buf = Buffer.alloc(4); + // above the max + assert.throwsNodeErrorWithMessage(() => buf.writeInt8(128), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 128 is out of range.'); + // below the min + assert.throwsNodeErrorWithMessage(() => buf.writeInt8(-129), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -129 is out of range.'); + `, + }, + { + name: "writing and then reading different sections of buffer", + script: ` + const buf = Buffer.alloc(3); + buf.writeInt8(3); // default offset of zero + buf.writeInt8(2, 1); + buf.writeInt8(1, 2); + + assertValueRead(buf.readInt8(0), 3); + assertValueRead(buf.readInt8(1), 2); + assertValueRead(buf.readInt8(2), 1); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeInt16BE(t *testing.T) { + tcs := []testCase{ + { + name: "write with out of range offset", + script: ` + const buf = Buffer.alloc(2); + assert.throwsNodeErrorWithMessage(() => buf.writeInt16BE(2, 1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 1 is out of range.'); + `, + }, + { + name: "number exceeds ranges", + script: ` + const buf = Buffer.alloc(4); + // above the max + assert.throwsNodeErrorWithMessage(() => buf.writeInt16BE(32768), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 32768 is out of range.'); + // below the min + assert.throwsNodeErrorWithMessage(() => buf.writeInt16BE(-32769), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -32769 is out of range.'); + `, + }, + { + name: "writing and then reading different sections of buffer", + script: ` + const buf = Buffer.alloc(6); + buf.writeInt16BE(3); // default offset of zero + buf.writeInt16BE(2, 2); + buf.writeInt16BE(1, 4); + + assertValueRead(buf.readInt16BE(0), 3); + assertValueRead(buf.readInt16BE(2), 2); + assertValueRead(buf.readInt16BE(4), 1); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeInt16LE(t *testing.T) { + tcs := []testCase{ + { + name: "write with out of range offset", + script: ` + const buf = Buffer.alloc(2); + assert.throwsNodeErrorWithMessage(() => buf.writeInt16LE(2, 1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 1 is out of range.'); + `, + }, + { + name: "number exceeds ranges", + script: ` + const buf = Buffer.alloc(4); + // above the max + assert.throwsNodeErrorWithMessage(() => buf.writeInt16LE(32768), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 32768 is out of range.'); + // below the min + assert.throwsNodeErrorWithMessage(() => buf.writeInt16LE(-32769), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -32769 is out of range.'); + `, + }, + { + name: "writing and then reading different sections of buffer", + script: ` + const buf = Buffer.alloc(6); + buf.writeInt16LE(3); // default offset of zero + buf.writeInt16LE(2, 2); + buf.writeInt16LE(1, 4); + + assertValueRead(buf.readInt16LE(0), 3); + assertValueRead(buf.readInt16LE(2), 2); + assertValueRead(buf.readInt16LE(4), 1); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeInt32BE(t *testing.T) { + tcs := []testCase{ + { + name: "write with out of range offset", + script: ` + const buf = Buffer.alloc(4); + assert.throwsNodeErrorWithMessage(() => buf.writeInt32BE(2, 1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 1 is out of range.'); + `, + }, + { + name: "number exceeds ranges", + script: ` + const buf = Buffer.alloc(4); + // above the max + assert.throwsNodeErrorWithMessage(() => buf.writeInt32BE(2147483648), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 2147483648 is out of range.'); + // below the min + assert.throwsNodeErrorWithMessage(() => buf.writeInt32BE(-2147483649), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -2147483649 is out of range.'); + `, + }, + { + name: "writing and then reading different sections of buffer", + script: ` + const buf = Buffer.alloc(12); + buf.writeInt32BE(3); // default offset of zero + buf.writeInt32BE(2, 4); + buf.writeInt32BE(1, 8); + + assertValueRead(buf.readInt32BE(0), 3); + assertValueRead(buf.readInt32BE(4), 2); + assertValueRead(buf.readInt32BE(8), 1); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeInt32LE(t *testing.T) { + tcs := []testCase{ + { + name: "write with out of range offset", + script: ` + const buf = Buffer.alloc(4); + assert.throwsNodeErrorWithMessage(() => buf.writeInt32LE(2, 1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 1 is out of range.'); + `, + }, + { + name: "number exceeds ranges", + script: ` + const buf = Buffer.alloc(4); + // above the max + assert.throwsNodeErrorWithMessage(() => buf.writeInt32LE(2147483648), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 2147483648 is out of range.'); + // below the min + assert.throwsNodeErrorWithMessage(() => buf.writeInt32LE(-2147483649), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -2147483649 is out of range.'); + `, + }, + { + name: "writing and then reading different sections of buffer", + script: ` + const buf = Buffer.alloc(12); + buf.writeInt32LE(3); // default offset of zero + buf.writeInt32LE(2, 4); + buf.writeInt32LE(1, 8); + + assertValueRead(buf.readInt32LE(0), 3); + assertValueRead(buf.readInt32LE(4), 2); + assertValueRead(buf.readInt32LE(8), 1); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeIntBE(t *testing.T) { + tcs := []testCase{ + { + name: "write out of range offset", + script: ` + const buf = Buffer.alloc(6); + assert.throwsNodeErrorWithMessage(() => buf.writeIntBE(127, 6, 1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 6 is out of range.'); + `, + }, + { + name: "byteLength out of range", + script: ` + const buf = Buffer.alloc(6); + + // above the max + assert.throwsNodeErrorWithMessage(() => buf.writeIntBE(0, 0, 7), RangeError, "ERR_OUT_OF_RANGE", 'The value of "byteLength" 7 is out of range.'); + // below the min + assert.throwsNodeErrorWithMessage(() => buf.writeIntBE(0, 0, 0), RangeError, "ERR_OUT_OF_RANGE", 'The value of "byteLength" 0 is out of range.'); + `, + }, + { + name: "number exceeds ranges", + script: ` + const buf = Buffer.alloc(6); + // above the 6-byte max + assert.throwsNodeErrorWithMessage(() => buf.writeIntBE(140737488355328, 0, 6), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 140737488355328 is out of range.'); + // below the 6-byte min + assert.throwsNodeErrorWithMessage(() => buf.writeIntBE(-140737488355329, 0, 6), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -140737488355329 is out of range.'); + // above the 2-byte max + assert.throwsNodeErrorWithMessage(() => buf.writeIntBE(32768, 0, 2), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 32768 is out of range.'); + // below the 2-byte min + assert.throwsNodeErrorWithMessage(() => buf.writeIntBE(-32769, 0, 2), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -32769 is out of range.'); + `, + }, + { + name: "writing and reading with different byte lengths", + script: ` + const buf = Buffer.alloc(6); + buf.writeIntBE(-1, 0, 1); // 1 byte + buf.writeIntBE(256, 1, 2); // 2 bytes + buf.writeIntBE(97328, 3, 3); // 3 bytes + + assertValueRead(buf.readIntBE(0, 1), -1); + assertValueRead(buf.toString('hex', 0, 1), "ff"); + assertValueRead(buf.readIntBE(1, 2), 256); + assertValueRead(buf.toString('hex', 1, 3), "0100"); + assertValueRead(buf.readIntBE(3, 3), 97328); + assertValueRead(buf.toString('hex', 3, 6), "017c30"); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeIntLE(t *testing.T) { + tcs := []testCase{ + { + name: "write out of range offset", + script: ` + const buf = Buffer.alloc(6); + assert.throwsNodeErrorWithMessage(() => buf.writeIntLE(127, 6, 1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 6 is out of range.'); + `, + }, + { + name: "byteLength out of range", + script: ` + const buf = Buffer.alloc(6); + + // above the max + assert.throwsNodeErrorWithMessage(() => buf.writeIntLE(0, 0, 7), RangeError, "ERR_OUT_OF_RANGE", 'The value of "byteLength" 7 is out of range.'); + // below the min + assert.throwsNodeErrorWithMessage(() => buf.writeIntLE(0, 0, 0), RangeError, "ERR_OUT_OF_RANGE", 'The value of "byteLength" 0 is out of range.'); + `, + }, + { + name: "number exceeds ranges", + script: ` + const buf = Buffer.alloc(6); + // above the 6-byte max + assert.throwsNodeErrorWithMessage(() => buf.writeIntLE(140737488355328, 0, 6), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 140737488355328 is out of range.'); + // below the 6-byte min + assert.throwsNodeErrorWithMessage(() => buf.writeIntLE(-140737488355329, 0, 6), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -140737488355329 is out of range.'); + // above the 2-byte max + assert.throwsNodeErrorWithMessage(() => buf.writeIntLE(32768, 0, 2), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 32768 is out of range.'); + // below the 2-byte min + assert.throwsNodeErrorWithMessage(() => buf.writeIntLE(-32769, 0, 2), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -32769 is out of range.'); + `, + }, + { + name: "writing and reading with different byte lengths", + script: ` + const buf = Buffer.alloc(6); + buf.writeIntLE(-1, 0, 1); // 1 byte + buf.writeIntLE(256, 1, 2); // 2 bytes + buf.writeIntLE(97328, 3, 3); // 3 bytes + + assertValueRead(buf.readIntLE(0, 1), -1); + assertValueRead(buf.toString('hex', 0, 1), "ff"); + assertValueRead(buf.readIntLE(1, 2), 256); + assertValueRead(buf.toString('hex', 1, 3), "0001"); + assertValueRead(buf.readIntLE(3, 3), 97328); + assertValueRead(buf.toString('hex', 3, 6), "307c01"); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeUInt8(t *testing.T) { + tcs := []testCase{ + { + name: "write with out of range offset", + script: ` + const buf = Buffer.alloc(2); + assert.throwsNodeErrorWithMessage(() => buf.writeUInt8(2, 2), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 2 is out of range.'); + `, + }, + { + name: "number exceeds ranges", + script: ` + const buf = Buffer.alloc(2); + assert.throwsNodeErrorWithMessage(() => buf.writeUInt8(256), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 256 is out of range.'); + assert.throwsNodeErrorWithMessage(() => buf.writeUInt8(256), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 256 is out of range.'); + `, + }, + { + name: "writing and then reading different sections of buffer", + script: ` + const buf = Buffer.alloc(3); + buf.writeUInt8(3); // default offset of zero + buf.writeUint8(2, 1); // using alias + buf.writeUInt8(1, 2); + + assertValueRead(buf.readUInt8(0), 3); + assertValueRead(buf.readUInt8(1), 2); + assertValueRead(buf.readUInt8(2), 1); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeUInt16BE(t *testing.T) { + tcs := []testCase{ + { + name: "write with out of range offset", + script: ` + const buf = Buffer.alloc(2); + assert.throwsNodeErrorWithMessage(() => buf.writeUInt16BE(2, 2), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 2 is out of range.'); + `, + }, + { + name: "number exceeds ranges", + script: ` + const buf = Buffer.alloc(2); + assert.throwsNodeErrorWithMessage(() => buf.writeUInt16BE(65536), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 65536 is out of range.'); + assert.throwsNodeErrorWithMessage(() => buf.writeUInt16BE(-1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -1 is out of range.'); + `, + }, + { + name: "writing and then reading different sections of buffer", + script: ` + const buf = Buffer.alloc(6); + buf.writeUInt16BE(3); // default offset of zero + buf.writeUInt16BE(2, 2); // using method alias + buf.writeUInt16BE(1, 4); + + assertValueRead(buf.readUInt16BE(0), 3); + assertValueRead(buf.readUInt16BE(2), 2); + assertValueRead(buf.readUInt16BE(4), 1); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeUInt16LE(t *testing.T) { + tcs := []testCase{ + { + name: "write with out of range offset", + script: ` + const buf = Buffer.alloc(2); + assert.throwsNodeErrorWithMessage(() => buf.writeUInt16LE(2, 2), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 2 is out of range.'); + `, + }, + { + name: "number exceeds ranges", + script: ` + const buf = Buffer.alloc(2); + assert.throwsNodeErrorWithMessage(() => buf.writeUInt16LE(65536), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 65536 is out of range.'); + assert.throwsNodeErrorWithMessage(() => buf.writeUInt16LE(-1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -1 is out of range.'); + `, + }, + { + name: "writing and then reading different sections of buffer", + script: ` + const buf = Buffer.alloc(6); + buf.writeUInt16LE(3); // default offset of zero + buf.writeUint16LE(2, 2); // using method alias + buf.writeUInt16LE(1, 4); + + assertValueRead(buf.readUInt16LE(0), 3); + assertValueRead(buf.readUInt16LE(2), 2); + assertValueRead(buf.readUInt16LE(4), 1); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeUInt32BE(t *testing.T) { + tcs := []testCase{ + { + name: "write with out of range offset", + script: ` + const buf = Buffer.alloc(4); + assert.throwsNodeErrorWithMessage(() => buf.writeUInt32BE(2, 2), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 2 is out of range.'); + `, + }, + { + name: "number exceeds ranges", + script: ` + const buf = Buffer.alloc(4); + assert.throwsNodeErrorWithMessage(() => buf.writeUInt32BE(4294967296), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 4294967296 is out of range.'); + assert.throwsNodeErrorWithMessage(() => buf.writeUInt32BE(-1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -1 is out of range.'); + `, + }, + { + name: "writing and then reading different sections of buffer", + script: ` + const buf = Buffer.alloc(12); + buf.writeUInt32BE(3); // default offset of zero + buf.writeUint32BE(2, 4); // using method alias + buf.writeUInt32BE(1, 8); + + assertValueRead(buf.readUInt32BE(0), 3); + assertValueRead(buf.readUInt32BE(4), 2); + assertValueRead(buf.readUInt32BE(8), 1); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeUInt32LE(t *testing.T) { + tcs := []testCase{ + { + name: "write with out of range offset", + script: ` + const buf = Buffer.alloc(4); + assert.throwsNodeErrorWithMessage(() => buf.writeUInt32LE(2, 2), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 2 is out of range.'); + `, + }, + { + name: "number exceeds ranges", + script: ` + const buf = Buffer.alloc(4); + assert.throwsNodeErrorWithMessage(() => buf.writeUInt32LE(4294967296), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 4294967296 is out of range.'); + assert.throwsNodeErrorWithMessage(() => buf.writeUInt32LE(-1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -1 is out of range.'); + `, + }, + { + name: "writing and then reading different sections of buffer", + script: ` + const buf = Buffer.alloc(12); + buf.writeUInt32LE(3); // default offset of zero + buf.writeUInt32LE(2, 4); // using method alias + buf.writeUInt32LE(1, 8); + + assertValueRead(buf.readUInt32LE(0), 3); + assertValueRead(buf.readUInt32LE(4), 2); + assertValueRead(buf.readUInt32LE(8), 1); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeUIntBE(t *testing.T) { + tcs := []testCase{ + { + name: "write out of range offset", + script: ` + const buf = Buffer.alloc(6); + assert.throwsNodeErrorWithMessage(() => buf.writeUIntBE(127, 6, 1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 6 is out of range.'); + `, + }, + { + name: "byteLength out of range", + script: ` + const buf = Buffer.alloc(6); + + // above the max + assert.throwsNodeErrorWithMessage(() => buf.writeUIntBE(0, 0, 7), RangeError, "ERR_OUT_OF_RANGE", 'The value of "byteLength" 7 is out of range.'); + // below the min + assert.throwsNodeErrorWithMessage(() => buf.writeUIntBE(0, 0, 0), RangeError, "ERR_OUT_OF_RANGE", 'The value of "byteLength" 0 is out of range.'); + `, + }, + { + name: "number exceeds ranges", + script: ` + const buf = Buffer.alloc(6); + // above the 6-byte max + assert.throwsNodeErrorWithMessage(() => buf.writeUIntBE(281474976710656, 0, 6), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 281474976710656 is out of range.'); + // below the 6-byte min + assert.throwsNodeErrorWithMessage(() => buf.writeUIntBE(-1, 0, 6), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -1 is out of range.'); + // above the 2-byte max + assert.throwsNodeErrorWithMessage(() => buf.writeUIntBE(65536, 0, 2), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 65536 is out of range.'); + // below the 2-byte min + assert.throwsNodeErrorWithMessage(() => buf.writeUIntBE(-1, 0, 2), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -1 is out of range.'); + `, + }, + { + name: "writing and reading with different byte lengths", + script: ` + const buf = Buffer.alloc(6); + buf.writeUIntBE(1, 0, 1); // 1 byte + buf.writeUintBE(256, 1, 2); // 2 bytes + buf.writeUIntBE(97328, 3, 3); // 3 bytes + + assertValueRead(buf.readUIntBE(0, 1), 1); + assertValueRead(buf.toString('hex', 0, 1), "01"); + assertValueRead(buf.readUIntBE(1, 2), 256); + assertValueRead(buf.toString('hex', 1, 3), "0100"); + assertValueRead(buf.readUIntBE(3, 3), 97328); + assertValueRead(buf.toString('hex', 3, 6), "017c30"); + `, + }, + } + + runTestCases(t, tcs) +} + +func TestBuffer_writeUIntLE(t *testing.T) { + tcs := []testCase{ + { + name: "write out of range offset", + script: ` + const buf = Buffer.alloc(6); + assert.throwsNodeErrorWithMessage(() => buf.writeUIntLE(127, 6, 1), RangeError, "ERR_OUT_OF_RANGE", 'The value of "offset" 6 is out of range.'); + `, + }, + { + name: "byteLength out of range", + script: ` + const buf = Buffer.alloc(6); + + // above the max + assert.throwsNodeErrorWithMessage(() => buf.writeUIntLE(0, 0, 7), RangeError, "ERR_OUT_OF_RANGE", 'The value of "byteLength" 7 is out of range.'); + // below the min + assert.throwsNodeErrorWithMessage(() => buf.writeUIntLE(0, 0, 0), RangeError, "ERR_OUT_OF_RANGE", 'The value of "byteLength" 0 is out of range.'); + `, + }, + { + name: "number exceeds ranges", + script: ` + const buf = Buffer.alloc(6); + // above the 6-byte max + assert.throwsNodeErrorWithMessage(() => buf.writeUIntLE(281474976710656, 0, 6), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 281474976710656 is out of range.'); + // below the 6-byte min + assert.throwsNodeErrorWithMessage(() => buf.writeUIntLE(-1, 0, 6), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -1 is out of range.'); + // above the 2-byte max + assert.throwsNodeErrorWithMessage(() => buf.writeUIntLE(65536, 0, 2), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" 65536 is out of range.'); + // below the 2-byte min + assert.throwsNodeErrorWithMessage(() => buf.writeUIntLE(-1, 0, 2), RangeError, "ERR_OUT_OF_RANGE", 'The value of "value" -1 is out of range.'); + `, + }, + { + name: "writing and reading with different byte lengths", + script: ` + const buf = Buffer.alloc(6); + buf.writeUIntLE(1, 0, 1); // 1 byte + buf.writeUintLE(256, 1, 2); // 2 bytes + buf.writeUIntLE(97328, 3, 3); // 3 bytes + + assertValueRead(buf.readUIntLE(0, 1), 1); + assertValueRead(buf.toString('hex', 0, 1), "01"); + assertValueRead(buf.readUIntLE(1, 2), 256); + assertValueRead(buf.toString('hex', 1, 3), "0001"); + assertValueRead(buf.readUIntLE(3, 3), 97328); + assertValueRead(buf.toString('hex', 3, 6), "307c01"); + `, + }, + } + + runTestCases(t, tcs) +} diff --git a/buffer/testdata/assertions.js b/buffer/testdata/assertions.js new file mode 100644 index 0000000..68fc695 --- /dev/null +++ b/buffer/testdata/assertions.js @@ -0,0 +1,34 @@ +/** + * Assertion helper functions for Buffer tests + */ +"use strict"; + +const assert = require("../../assert.js"); + +function assertValueRead(actual, expected) { + assert.sameValue(actual, expected, "value read does not match; ") +} + +function assertBytesWritten(actual, expected) { + assert.sameValue(actual, expected, "bytesWritten does not match; ") +} + +function assertBufferWriteRead(buffer, writeMethod, readMethod, value, offset = 0) { + const bytesWritten = buffer[writeMethod](value, offset); + const bytesPerElement = getBufferElementSize(writeMethod); + assertBytesWritten(bytesWritten, offset + bytesPerElement); + + const readValue = buffer[readMethod](offset); + assertValueRead(readValue, value); +} + +// getBufferElementSize determines the number of bytes per type based on method name +function getBufferElementSize(methodName) { + if (methodName.includes('64')) return 8; + if (methodName.includes('Double')) return 8; + if (methodName.includes('32')) return 4; + if (methodName.includes('Float')) return 4; + if (methodName.includes('16')) return 2; + if (methodName.includes('8')) return 1; + return 1; +} diff --git a/buffer/types/README.md b/buffer/types/README.md new file mode 100644 index 0000000..f1aa8a6 --- /dev/null +++ b/buffer/types/README.md @@ -0,0 +1,10 @@ +## Type definitions for the goja_nodejs buffer module. + +This package contains type definitions which only include features +currently implemented by the goja_nodejs buffer module. + +### Install + +```shell +npm install --save-dev @dop251/types-goja_nodejs-buffer +``` diff --git a/buffer/types/buffer.buffer.d.ts b/buffer/types/buffer.buffer.d.ts new file mode 100644 index 0000000..4e39db2 --- /dev/null +++ b/buffer/types/buffer.buffer.d.ts @@ -0,0 +1,210 @@ +declare module "buffer" { + type ImplicitArrayBuffer> = T extends + { valueOf(): infer V extends ArrayBufferLike } ? V : T; + global { + interface BufferConstructor { + // see buffer.d.ts for implementation shared with all TypeScript versions + + /** + * Allocates a new buffer containing the given {str}. + * + * @param str String to store in buffer. + * @param encoding encoding to use, optional. Default is 'utf8' + * @deprecated since v10.0.0 - Use `Buffer.from(string[, encoding])` instead. + */ + new(str: string, encoding?: BufferEncoding): Buffer; + /** + * Allocates a new buffer containing the given {array} of octets. + * + * @param array The octets to store. + * @deprecated since v10.0.0 - Use `Buffer.from(array)` instead. + */ + new(array: ArrayLike): Buffer; + /** + * Produces a Buffer backed by the same allocated memory as + * the given {ArrayBuffer}/{SharedArrayBuffer}. + * + * @param arrayBuffer The ArrayBuffer with which to share memory. + * @deprecated since v10.0.0 - Use `Buffer.from(arrayBuffer[, byteOffset[, length]])` instead. + */ + new(arrayBuffer: TArrayBuffer): Buffer; + /** + * Allocates a new `Buffer` using an `array` of bytes in the range `0` – `255`. + * Array entries outside that range will be truncated to fit into it. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * // Creates a new Buffer containing the UTF-8 bytes of the string 'buffer'. + * const buf = Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]); + * ``` + * + * If `array` is an `Array`-like object (that is, one with a `length` property of + * type `number`), it is treated as if it is an array, unless it is a `Buffer` or + * a `Uint8Array`. This means all other `TypedArray` variants get treated as an + * `Array`. To create a `Buffer` from the bytes backing a `TypedArray`, use + * `Buffer.copyBytesFrom()`. + * + * A `TypeError` will be thrown if `array` is not an `Array` or another type + * appropriate for `Buffer.from()` variants. + * + * `Buffer.from(array)` and `Buffer.from(string)` may also use the internal + * `Buffer` pool like `Buffer.allocUnsafe()` does. + * @since v5.10.0 + */ + from(array: WithImplicitCoercion>): Buffer; + /** + * This creates a view of the `ArrayBuffer` without copying the underlying + * memory. For example, when passed a reference to the `.buffer` property of a + * `TypedArray` instance, the newly created `Buffer` will share the same + * allocated memory as the `TypedArray`'s underlying `ArrayBuffer`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const arr = new Uint16Array(2); + * + * arr[0] = 5000; + * arr[1] = 4000; + * + * // Shares memory with `arr`. + * const buf = Buffer.from(arr.buffer); + * + * console.log(buf); + * // Prints: + * + * // Changing the original Uint16Array changes the Buffer also. + * arr[1] = 6000; + * + * console.log(buf); + * // Prints: + * ``` + * + * The optional `byteOffset` and `length` arguments specify a memory range within + * the `arrayBuffer` that will be shared by the `Buffer`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const ab = new ArrayBuffer(10); + * const buf = Buffer.from(ab, 0, 2); + * + * console.log(buf.length); + * // Prints: 2 + * ``` + * + * A `TypeError` will be thrown if `arrayBuffer` is not an `ArrayBuffer` or a + * `SharedArrayBuffer` or another type appropriate for `Buffer.from()` + * variants. + * + * It is important to remember that a backing `ArrayBuffer` can cover a range + * of memory that extends beyond the bounds of a `TypedArray` view. A new + * `Buffer` created using the `buffer` property of a `TypedArray` may extend + * beyond the range of the `TypedArray`: + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const arrA = Uint8Array.from([0x63, 0x64, 0x65, 0x66]); // 4 elements + * const arrB = new Uint8Array(arrA.buffer, 1, 2); // 2 elements + * console.log(arrA.buffer === arrB.buffer); // true + * + * const buf = Buffer.from(arrB.buffer); + * console.log(buf); + * // Prints: + * ``` + * @since v5.10.0 + * @param arrayBuffer An `ArrayBuffer`, `SharedArrayBuffer`, for example the + * `.buffer` property of a `TypedArray`. + * @param byteOffset Index of first byte to expose. **Default:** `0`. + * @param length Number of bytes to expose. **Default:** + * `arrayBuffer.byteLength - byteOffset`. + */ + from>( + arrayBuffer: TArrayBuffer, + byteOffset?: number, + length?: number, + ): Buffer>; + /** + * Creates a new `Buffer` containing `string`. The `encoding` parameter identifies + * the character encoding to be used when converting `string` into bytes. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf1 = Buffer.from('this is a tést'); + * const buf2 = Buffer.from('7468697320697320612074c3a97374', 'hex'); + * + * console.log(buf1.toString()); + * // Prints: this is a tést + * console.log(buf2.toString()); + * // Prints: this is a tést + * console.log(buf1.toString('latin1')); + * // Prints: this is a tést + * ``` + * + * A `TypeError` will be thrown if `string` is not a string or another type + * appropriate for `Buffer.from()` variants. + * + * `Buffer.from(string)` may also use the internal `Buffer` pool like + * `Buffer.allocUnsafe()` does. + * @since v5.10.0 + * @param string A string to encode. + * @param encoding The encoding of `string`. **Default:** `'utf8'`. + */ + from(string: WithImplicitCoercion, encoding?: BufferEncoding): Buffer; + /** + * Allocates a new `Buffer` of `size` bytes. If `fill` is `undefined`, the`Buffer` will be zero-filled. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.alloc(5); + * + * console.log(buf); + * // Prints: + * ``` + * + * If `size` is larger than {@link constants.MAX_LENGTH} or smaller than 0, `ERR_OUT_OF_RANGE` is thrown. + * + * If `fill` is specified, the allocated `Buffer` will be initialized by calling `buf.fill(fill)`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.alloc(5, 'a'); + * + * console.log(buf); + * // Prints: + * ``` + * + * If both `fill` and `encoding` are specified, the allocated `Buffer` will be + * initialized by calling `buf.fill(fill, encoding)`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.alloc(11, 'aGVsbG8gd29ybGQ=', 'base64'); + * + * console.log(buf); + * // Prints: + * ``` + * + * Calling `Buffer.alloc()` can be measurably slower than the alternative `Buffer.allocUnsafe()` but ensures that the newly created `Buffer` instance + * contents will never contain sensitive data from previous allocations, including + * data that might not have been allocated for `Buffer`s. + * + * A `TypeError` will be thrown if `size` is not a number. + * @since v5.10.0 + * @param size The desired length of the new `Buffer`. + * @param [fill=0] A value to pre-fill the new `Buffer` with. + * @param [encoding='utf8'] If `fill` is a string, this is its encoding. + */ + alloc(size: number, fill?: string | Uint8Array | number, encoding?: BufferEncoding): Buffer; + } + interface Buffer extends Uint8Array { + // see buffer.d.ts for implementation shared with all TypeScript versions + + } + } +} diff --git a/buffer/types/buffer.d.ts b/buffer/types/buffer.d.ts new file mode 100644 index 0000000..d91db47 --- /dev/null +++ b/buffer/types/buffer.d.ts @@ -0,0 +1,1318 @@ +/// +/// +declare module 'buffer' { + export type WithImplicitCoercion = + | T + | { valueOf(): T } + | (T extends string ? { [Symbol.toPrimitive](hint: "string"): T } : never); + + export { Buffer }; + + global { + type BufferEncoding = + // | "ascii" + | "utf8" + | "utf-8" +// | "utf16le" +// | "utf-16le" +// | "ucs2" +// | "ucs-2" + | "base64" + | "base64url" +// | "latin1" +// | "binary" + | "hex"; + + /** + * Raw data is stored in instances of the Buffer class. + * A Buffer is similar to an array of integers but corresponds to a raw memory allocation outside the V8 heap. A Buffer cannot be resized. + * Valid string encodings: 'ascii'|'utf8'|'utf16le'|'ucs2'(alias of 'utf16le')|'base64'|'base64url'|'binary'(deprecated)|'hex' + */ + interface BufferConstructor { + // see buffer.buffer.d.ts for implementation specific to TypeScript 5.7 and later + // see ts5.6/buffer.buffer.d.ts for implementation specific to TypeScript 5.6 and earlier + + /** + * Returns `true` if `obj` is a `Buffer`, `false` otherwise. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * Buffer.isBuffer(Buffer.alloc(10)); // true + * Buffer.isBuffer(Buffer.from('foo')); // true + * Buffer.isBuffer('a string'); // false + * Buffer.isBuffer([]); // false + * Buffer.isBuffer(new Uint8Array(1024)); // false + * ``` + * @since v0.1.101 + */ + isBuffer(obj: any): obj is Buffer; + + /** + * Returns `true` if `encoding` is the name of a supported character encoding, + * or `false` otherwise. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * console.log(Buffer.isEncoding('utf8')); + * // Prints: true + * + * console.log(Buffer.isEncoding('hex')); + * // Prints: true + * + * console.log(Buffer.isEncoding('utf/8')); + * // Prints: false + * + * console.log(Buffer.isEncoding('')); + * // Prints: false + * ``` + * @since v0.9.1 + * @param encoding A character encoding name to check. + */ + isEncoding(encoding: string): encoding is BufferEncoding; + + /** + * Returns the byte length of a string when encoded using `encoding`. + * This is not the same as [`String.prototype.length`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length), which does not account + * for the encoding that is used to convert the string into bytes. + * + * For `'base64'`, `'base64url'`, and `'hex'`, this function assumes valid input. + * For strings that contain non-base64/hex-encoded data (e.g. whitespace), the + * return value might be greater than the length of a `Buffer` created from the + * string. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const str = '\u00bd + \u00bc = \u00be'; + * + * console.log(`${str}: ${str.length} characters, ` + + * `${Buffer.byteLength(str, 'utf8')} bytes`); + * // Prints: ½ + ¼ = ¾: 9 characters, 12 bytes + * ``` + * + * When `string` is a + * `Buffer`/[`DataView`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView)/[`TypedArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/- + * Reference/Global_Objects/TypedArray)/[`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer)/[`SharedArrayBuffer`](https://develop- + * er.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer), the byte length as reported by `.byteLength`is returned. + * @since v0.1.90 + * @param string A value to calculate the length of. + * @param [encoding='utf8'] If `string` is a string, this is its encoding. + * @return The number of bytes contained within `string`. + */ + byteLength( + string: string | Buffer | ArrayBuffer, + encoding?: BufferEncoding, + ): number; + + /** + * Compares `buf1` to `buf2`, typically for the purpose of sorting arrays of `Buffer` instances. This is equivalent to calling `buf1.compare(buf2)`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf1 = Buffer.from('1234'); + * const buf2 = Buffer.from('0123'); + * const arr = [buf1, buf2]; + * + * console.log(arr.sort(Buffer.compare)); + * // Prints: [ , ] + * // (This result is equal to: [buf2, buf1].) + * ``` + * @since v0.11.13 + * @return Either `-1`, `0`, or `1`, depending on the result of the comparison. See `compare` for details. + */ + compare(buf1: Uint8Array, buf2: Uint8Array): -1 | 0 | 1; + + /** + * This is the size (in bytes) of pre-allocated internal `Buffer` instances used + * for pooling. This value may be modified. + * @since v0.11.3 + */ + poolSize: number; + } + + interface Buffer { + /** + * Writes `string` to `buf` at `offset` according to the character encoding in`encoding`. The `length` parameter is the number of bytes to write. If `buf` did + * not contain enough space to fit the entire string, only part of `string` will be + * written. However, partially encoded characters will not be written. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.alloc(256); + * + * const len = buf.write('\u00bd + \u00bc = \u00be', 0); + * + * console.log(`${len} bytes: ${buf.toString('utf8', 0, len)}`); + * // Prints: 12 bytes: ½ + ¼ = ¾ + * + * const buffer = Buffer.alloc(10); + * + * const length = buffer.write('abcd', 8); + * + * console.log(`${length} bytes: ${buffer.toString('utf8', 8, 10)}`); + * // Prints: 2 bytes : ab + * ``` + * @since v0.1.90 + * @param string String to write to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write `string`. + * @param [length=buf.length - offset] Maximum number of bytes to write (written bytes will not exceed `buf.length - offset`). + * @param [encoding='utf8'] The character encoding of `string`. + * @return Number of bytes written. + */ + write(string: string, encoding?: BufferEncoding): number; + + write(string: string, offset: number, encoding?: BufferEncoding): number; + + write(string: string, offset: number, length: number, encoding?: BufferEncoding): number; + + /** + * Decodes `buf` to a string according to the specified character encoding in`encoding`. `start` and `end` may be passed to decode only a subset of `buf`. + * + * If `encoding` is `'utf8'` and a byte sequence in the input is not valid UTF-8, + * then each invalid byte is replaced with the replacement character `U+FFFD`. + * + * The maximum length of a string instance (in UTF-16 code units) is available + * as {@link constants.MAX_STRING_LENGTH}. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf1 = Buffer.allocUnsafe(26); + * + * for (let i = 0; i < 26; i++) { + * // 97 is the decimal ASCII value for 'a'. + * buf1[i] = i + 97; + * } + * + * console.log(buf1.toString('utf8')); + * // Prints: abcdefghijklmnopqrstuvwxyz + * console.log(buf1.toString('utf8', 0, 5)); + * // Prints: abcde + * + * const buf2 = Buffer.from('tést'); + * + * console.log(buf2.toString('hex')); + * // Prints: 74c3a97374 + * console.log(buf2.toString('utf8', 0, 3)); + * // Prints: té + * console.log(buf2.toString(undefined, 0, 3)); + * // Prints: té + * ``` + * @since v0.1.90 + * @param [encoding='utf8'] The character encoding to use. + * @param [start=0] The byte offset to start decoding at. + * @param [end=buf.length] The byte offset to stop decoding at (not inclusive). + */ + toString(encoding?: BufferEncoding, start?: number, end?: number): string; + + /** + * Returns a JSON representation of `buf`. [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) implicitly calls + * this function when stringifying a `Buffer` instance. + * + * `Buffer.from()` accepts objects in the format returned from this method. + * In particular, `Buffer.from(buf.toJSON())` works like `Buffer.from(buf)`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5]); + * const json = JSON.stringify(buf); + * + * console.log(json); + * // Prints: {"type":"Buffer","data":[1,2,3,4,5]} + * + * const copy = JSON.parse(json, (key, value) => { + * return value && value.type === 'Buffer' ? + * Buffer.from(value) : + * value; + * }); + * + * console.log(copy); + * // Prints: + * ``` + * @since v0.9.2 + */ + // NOT IMPLEMENTED + // toJSON(): { + // type: "Buffer"; + // data: number[]; + // }; + + /** + * Returns `true` if both `buf` and `otherBuffer` have exactly the same bytes,`false` otherwise. Equivalent to `buf.compare(otherBuffer) === 0`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf1 = Buffer.from('ABC'); + * const buf2 = Buffer.from('414243', 'hex'); + * const buf3 = Buffer.from('ABCD'); + * + * console.log(buf1.equals(buf2)); + * // Prints: true + * console.log(buf1.equals(buf3)); + * // Prints: false + * ``` + * @since v0.11.13 + * @param otherBuffer A `Buffer` or {@link Uint8Array} with which to compare `buf`. + */ + equals(otherBuffer: Uint8Array): boolean; + + /** + * Writes `value` to `buf` at the specified `offset` as big-endian. + * + * `value` is interpreted and written as a two's complement signed integer. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(8); + * + * buf.writeBigInt64BE(0x0102030405060708n, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v12.0.0, v10.20.0 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy: `0 <= offset <= buf.length - 8`. + * @return `offset` plus the number of bytes written. + */ + writeBigInt64BE(value: bigint, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as little-endian. + * + * `value` is interpreted and written as a two's complement signed integer. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(8); + * + * buf.writeBigInt64LE(0x0102030405060708n, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v12.0.0, v10.20.0 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy: `0 <= offset <= buf.length - 8`. + * @return `offset` plus the number of bytes written. + */ + writeBigInt64LE(value: bigint, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as big-endian. + * + * This function is also available under the `writeBigUint64BE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(8); + * + * buf.writeBigUInt64BE(0xdecafafecacefaden, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v12.0.0, v10.20.0 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy: `0 <= offset <= buf.length - 8`. + * @return `offset` plus the number of bytes written. + */ + writeBigUInt64BE(value: bigint, offset?: number): number; + /** + * @alias Buffer.writeBigUInt64BE + * @since v14.10.0, v12.19.0 + */ + writeBigUint64BE(value: bigint, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as little-endian + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(8); + * + * buf.writeBigUInt64LE(0xdecafafecacefaden, 0); + * + * console.log(buf); + * // Prints: + * ``` + * + * This function is also available under the `writeBigUint64LE` alias. + * @since v12.0.0, v10.20.0 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy: `0 <= offset <= buf.length - 8`. + * @return `offset` plus the number of bytes written. + */ + writeBigUInt64LE(value: bigint, offset?: number): number; + /** + * @alias Buffer.writeBigUInt64LE + * @since v14.10.0, v12.19.0 + */ + writeBigUint64LE(value: bigint, offset?: number): number; + /** + * Writes `byteLength` bytes of `value` to `buf` at the specified `offset`as little-endian. Supports up to 48 bits of accuracy. Behavior is undefined + * when `value` is anything other than an unsigned integer. + * + * This function is also available under the `writeUintLE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(6); + * + * buf.writeUIntLE(0x1234567890ab, 0, 6); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param offset Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - byteLength`. + * @param byteLength Number of bytes to write. Must satisfy `0 < byteLength <= 6`. + * @return `offset` plus the number of bytes written. + */ + writeUIntLE(value: number, offset: number, byteLength: number): number; + /** + * @alias Buffer.writeUIntLE + * @since v14.9.0, v12.19.0 + */ + writeUintLE(value: number, offset: number, byteLength: number): number; + /** + * Writes `byteLength` bytes of `value` to `buf` at the specified `offset`as big-endian. Supports up to 48 bits of accuracy. Behavior is undefined + * when `value` is anything other than an unsigned integer. + * + * This function is also available under the `writeUintBE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(6); + * + * buf.writeUIntBE(0x1234567890ab, 0, 6); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param offset Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - byteLength`. + * @param byteLength Number of bytes to write. Must satisfy `0 < byteLength <= 6`. + * @return `offset` plus the number of bytes written. + */ + writeUIntBE(value: number, offset: number, byteLength: number): number; + /** + * @alias Buffer.writeUIntBE + * @since v14.9.0, v12.19.0 + */ + writeUintBE(value: number, offset: number, byteLength: number): number; + /** + * Writes `byteLength` bytes of `value` to `buf` at the specified `offset`as little-endian. Supports up to 48 bits of accuracy. Behavior is undefined + * when `value` is anything other than a signed integer. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(6); + * + * buf.writeIntLE(0x1234567890ab, 0, 6); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.11.15 + * @param value Number to be written to `buf`. + * @param offset Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - byteLength`. + * @param byteLength Number of bytes to write. Must satisfy `0 < byteLength <= 6`. + * @return `offset` plus the number of bytes written. + */ + writeIntLE(value: number, offset: number, byteLength: number): number; + /** + * Writes `byteLength` bytes of `value` to `buf` at the specified `offset`as big-endian. Supports up to 48 bits of accuracy. Behavior is undefined when`value` is anything other than a + * signed integer. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(6); + * + * buf.writeIntBE(0x1234567890ab, 0, 6); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.11.15 + * @param value Number to be written to `buf`. + * @param offset Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - byteLength`. + * @param byteLength Number of bytes to write. Must satisfy `0 < byteLength <= 6`. + * @return `offset` plus the number of bytes written. + */ + writeIntBE(value: number, offset: number, byteLength: number): number; + /** + * Reads an unsigned, big-endian 64-bit integer from `buf` at the specified`offset`. + * + * This function is also available under the `readBigUint64BE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff]); + * + * console.log(buf.readBigUInt64BE(0)); + * // Prints: 4294967295n + * ``` + * @since v12.0.0, v10.20.0 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy: `0 <= offset <= buf.length - 8`. + */ + readBigUInt64BE(offset?: number): bigint; + + /** + * @alias Buffer.readBigUInt64BE + * @since v14.10.0, v12.19.0 + */ + readBigUint64BE(offset?: number): bigint; + + /** + * Reads an unsigned, little-endian 64-bit integer from `buf` at the specified`offset`. + * + * This function is also available under the `readBigUint64LE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff]); + * + * console.log(buf.readBigUInt64LE(0)); + * // Prints: 18446744069414584320n + * ``` + * @since v12.0.0, v10.20.0 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy: `0 <= offset <= buf.length - 8`. + */ + readBigUInt64LE(offset?: number): bigint; + + /** + * @alias Buffer.readBigUInt64LE + * @since v14.10.0, v12.19.0 + */ + readBigUint64LE(offset?: number): bigint; + + /** + * Reads a signed, big-endian 64-bit integer from `buf` at the specified `offset`. + * + * Integers read from a `Buffer` are interpreted as two's complement signed + * values. + * @since v12.0.0, v10.20.0 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy: `0 <= offset <= buf.length - 8`. + */ + readBigInt64BE(offset?: number): bigint; + + /** + * Reads a signed, little-endian 64-bit integer from `buf` at the specified`offset`. + * + * Integers read from a `Buffer` are interpreted as two's complement signed + * values. + * @since v12.0.0, v10.20.0 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy: `0 <= offset <= buf.length - 8`. + */ + readBigInt64LE(offset?: number): bigint; + + /** + * Reads `byteLength` number of bytes from `buf` at the specified `offset` and interprets the result as an unsigned, little-endian integer supporting + * up to 48 bits of accuracy. + * + * This function is also available under the `readUintLE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + * + * console.log(buf.readUIntLE(0, 6).toString(16)); + * // Prints: ab9078563412 + * ``` + * @since v0.11.15 + * @param offset Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - byteLength`. + * @param byteLength Number of bytes to read. Must satisfy `0 < byteLength <= 6`. + */ + readUIntLE(offset: number, byteLength: number): number; + + /** + * @alias Buffer.readUIntLE + * @since v14.9.0, v12.19.0 + */ + readUintLE(offset: number, byteLength: number): number; + + /** + * Reads `byteLength` number of bytes from `buf` at the specified `offset` and interprets the result as an unsigned big-endian integer supporting + * up to 48 bits of accuracy. + * + * This function is also available under the `readUintBE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + * + * console.log(buf.readUIntBE(0, 6).toString(16)); + * // Prints: 1234567890ab + * console.log(buf.readUIntBE(1, 6).toString(16)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.11.15 + * @param offset Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - byteLength`. + * @param byteLength Number of bytes to read. Must satisfy `0 < byteLength <= 6`. + */ + readUIntBE(offset: number, byteLength: number): number; + + /** + * @alias Buffer.readUIntBE + * @since v14.9.0, v12.19.0 + */ + readUintBE(offset: number, byteLength: number): number; + + /** + * Reads `byteLength` number of bytes from `buf` at the specified `offset` and interprets the result as a little-endian, two's complement signed value + * supporting up to 48 bits of accuracy. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + * + * console.log(buf.readIntLE(0, 6).toString(16)); + * // Prints: -546f87a9cbee + * ``` + * @since v0.11.15 + * @param offset Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - byteLength`. + * @param byteLength Number of bytes to read. Must satisfy `0 < byteLength <= 6`. + */ + readIntLE(offset: number, byteLength: number): number; + + /** + * Reads `byteLength` number of bytes from `buf` at the specified `offset` and interprets the result as a big-endian, two's complement signed value + * supporting up to 48 bits of accuracy. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x12, 0x34, 0x56, 0x78, 0x90, 0xab]); + * + * console.log(buf.readIntBE(0, 6).toString(16)); + * // Prints: 1234567890ab + * console.log(buf.readIntBE(1, 6).toString(16)); + * // Throws ERR_OUT_OF_RANGE. + * console.log(buf.readIntBE(1, 0).toString(16)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.11.15 + * @param offset Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - byteLength`. + * @param byteLength Number of bytes to read. Must satisfy `0 < byteLength <= 6`. + */ + readIntBE(offset: number, byteLength: number): number; + + /** + * Reads an unsigned 8-bit integer from `buf` at the specified `offset`. + * + * This function is also available under the `readUint8` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([1, -2]); + * + * console.log(buf.readUInt8(0)); + * // Prints: 1 + * console.log(buf.readUInt8(1)); + * // Prints: 254 + * console.log(buf.readUInt8(2)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.5.0 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 1`. + */ + readUInt8(offset?: number): number; + + /** + * @alias Buffer.readUInt8 + * @since v14.9.0, v12.19.0 + */ + readUint8(offset?: number): number; + + /** + * Reads an unsigned, little-endian 16-bit integer from `buf` at the specified `offset`. + * + * This function is also available under the `readUint16LE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x12, 0x34, 0x56]); + * + * console.log(buf.readUInt16LE(0).toString(16)); + * // Prints: 3412 + * console.log(buf.readUInt16LE(1).toString(16)); + * // Prints: 5634 + * console.log(buf.readUInt16LE(2).toString(16)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.5.5 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 2`. + */ + readUInt16LE(offset?: number): number; + + /** + * @alias Buffer.readUInt16LE + * @since v14.9.0, v12.19.0 + */ + readUint16LE(offset?: number): number; + + /** + * Reads an unsigned, big-endian 16-bit integer from `buf` at the specified`offset`. + * + * This function is also available under the `readUint16BE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x12, 0x34, 0x56]); + * + * console.log(buf.readUInt16BE(0).toString(16)); + * // Prints: 1234 + * console.log(buf.readUInt16BE(1).toString(16)); + * // Prints: 3456 + * ``` + * @since v0.5.5 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 2`. + */ + readUInt16BE(offset?: number): number; + + /** + * @alias Buffer.readUInt16BE + * @since v14.9.0, v12.19.0 + */ + readUint16BE(offset?: number): number; + + /** + * Reads an unsigned, little-endian 32-bit integer from `buf` at the specified`offset`. + * + * This function is also available under the `readUint32LE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x12, 0x34, 0x56, 0x78]); + * + * console.log(buf.readUInt32LE(0).toString(16)); + * // Prints: 78563412 + * console.log(buf.readUInt32LE(1).toString(16)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.5.5 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 4`. + */ + readUInt32LE(offset?: number): number; + + /** + * @alias Buffer.readUInt32LE + * @since v14.9.0, v12.19.0 + */ + readUint32LE(offset?: number): number; + + /** + * Reads an unsigned, big-endian 32-bit integer from `buf` at the specified`offset`. + * + * This function is also available under the `readUint32BE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0x12, 0x34, 0x56, 0x78]); + * + * console.log(buf.readUInt32BE(0).toString(16)); + * // Prints: 12345678 + * ``` + * @since v0.5.5 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 4`. + */ + readUInt32BE(offset?: number): number; + + /** + * @alias Buffer.readUInt32BE + * @since v14.9.0, v12.19.0 + */ + readUint32BE(offset?: number): number; + + /** + * Reads a signed 8-bit integer from `buf` at the specified `offset`. + * + * Integers read from a `Buffer` are interpreted as two's complement signed values. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([-1, 5]); + * + * console.log(buf.readInt8(0)); + * // Prints: -1 + * console.log(buf.readInt8(1)); + * // Prints: 5 + * console.log(buf.readInt8(2)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.5.0 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 1`. + */ + readInt8(offset?: number): number; + + /** + * Reads a signed, little-endian 16-bit integer from `buf` at the specified`offset`. + * + * Integers read from a `Buffer` are interpreted as two's complement signed values. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0, 5]); + * + * console.log(buf.readInt16LE(0)); + * // Prints: 1280 + * console.log(buf.readInt16LE(1)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.5.5 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 2`. + */ + readInt16LE(offset?: number): number; + + /** + * Reads a signed, big-endian 16-bit integer from `buf` at the specified `offset`. + * + * Integers read from a `Buffer` are interpreted as two's complement signed values. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0, 5]); + * + * console.log(buf.readInt16BE(0)); + * // Prints: 5 + * ``` + * @since v0.5.5 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 2`. + */ + readInt16BE(offset?: number): number; + + /** + * Reads a signed, little-endian 32-bit integer from `buf` at the specified`offset`. + * + * Integers read from a `Buffer` are interpreted as two's complement signed values. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0, 0, 0, 5]); + * + * console.log(buf.readInt32LE(0)); + * // Prints: 83886080 + * console.log(buf.readInt32LE(1)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.5.5 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 4`. + */ + readInt32LE(offset?: number): number; + + /** + * Reads a signed, big-endian 32-bit integer from `buf` at the specified `offset`. + * + * Integers read from a `Buffer` are interpreted as two's complement signed values. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([0, 0, 0, 5]); + * + * console.log(buf.readInt32BE(0)); + * // Prints: 5 + * ``` + * @since v0.5.5 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 4`. + */ + readInt32BE(offset?: number): number; + + /** + * Reads a 32-bit, little-endian float from `buf` at the specified `offset`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([1, 2, 3, 4]); + * + * console.log(buf.readFloatLE(0)); + * // Prints: 1.539989614439558e-36 + * console.log(buf.readFloatLE(1)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.11.15 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 4`. + */ + readFloatLE(offset?: number): number; + + /** + * Reads a 32-bit, big-endian float from `buf` at the specified `offset`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([1, 2, 3, 4]); + * + * console.log(buf.readFloatBE(0)); + * // Prints: 2.387939260590663e-38 + * ``` + * @since v0.11.15 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 4`. + */ + readFloatBE(offset?: number): number; + + /** + * Reads a 64-bit, little-endian double from `buf` at the specified `offset`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]); + * + * console.log(buf.readDoubleLE(0)); + * // Prints: 5.447603722011605e-270 + * console.log(buf.readDoubleLE(1)); + * // Throws ERR_OUT_OF_RANGE. + * ``` + * @since v0.11.15 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 8`. + */ + readDoubleLE(offset?: number): number; + + /** + * Reads a 64-bit, big-endian double from `buf` at the specified `offset`. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]); + * + * console.log(buf.readDoubleBE(0)); + * // Prints: 8.20788039913184e-304 + * ``` + * @since v0.11.15 + * @param [offset=0] Number of bytes to skip before starting to read. Must satisfy `0 <= offset <= buf.length - 8`. + */ + readDoubleBE(offset?: number): number; + + /** + * Writes `value` to `buf` at the specified `offset`. `value` must be a + * valid unsigned 8-bit integer. Behavior is undefined when `value` is anything + * other than an unsigned 8-bit integer. + * + * This function is also available under the `writeUint8` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(4); + * + * buf.writeUInt8(0x3, 0); + * buf.writeUInt8(0x4, 1); + * buf.writeUInt8(0x23, 2); + * buf.writeUInt8(0x42, 3); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.0 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 1`. + * @return `offset` plus the number of bytes written. + */ + writeUInt8(value: number, offset?: number): number; + /** + * @alias Buffer.writeUInt8 + * @since v14.9.0, v12.19.0 + */ + writeUint8(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as little-endian. The `value` must be a valid unsigned 16-bit integer. Behavior is undefined when `value` is + * anything other than an unsigned 16-bit integer. + * + * This function is also available under the `writeUint16LE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(4); + * + * buf.writeUInt16LE(0xdead, 0); + * buf.writeUInt16LE(0xbeef, 2); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 2`. + * @return `offset` plus the number of bytes written. + */ + writeUInt16LE(value: number, offset?: number): number; + /** + * @alias Buffer.writeUInt16LE + * @since v14.9.0, v12.19.0 + */ + writeUint16LE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as big-endian. The `value` must be a valid unsigned 16-bit integer. Behavior is undefined when `value`is anything other than an + * unsigned 16-bit integer. + * + * This function is also available under the `writeUint16BE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(4); + * + * buf.writeUInt16BE(0xdead, 0); + * buf.writeUInt16BE(0xbeef, 2); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 2`. + * @return `offset` plus the number of bytes written. + */ + writeUInt16BE(value: number, offset?: number): number; + /** + * @alias Buffer.writeUInt16BE + * @since v14.9.0, v12.19.0 + */ + writeUint16BE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as little-endian. The `value` must be a valid unsigned 32-bit integer. Behavior is undefined when `value` is + * anything other than an unsigned 32-bit integer. + * + * This function is also available under the `writeUint32LE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(4); + * + * buf.writeUInt32LE(0xfeedface, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 4`. + * @return `offset` plus the number of bytes written. + */ + writeUInt32LE(value: number, offset?: number): number; + /** + * @alias Buffer.writeUInt32LE + * @since v14.9.0, v12.19.0 + */ + writeUint32LE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as big-endian. The `value` must be a valid unsigned 32-bit integer. Behavior is undefined when `value`is anything other than an + * unsigned 32-bit integer. + * + * This function is also available under the `writeUint32BE` alias. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(4); + * + * buf.writeUInt32BE(0xfeedface, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 4`. + * @return `offset` plus the number of bytes written. + */ + writeUInt32BE(value: number, offset?: number): number; + /** + * @alias Buffer.writeUInt32BE + * @since v14.9.0, v12.19.0 + */ + writeUint32BE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset`. `value` must be a valid + * signed 8-bit integer. Behavior is undefined when `value` is anything other than + * a signed 8-bit integer. + * + * `value` is interpreted and written as a two's complement signed integer. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(2); + * + * buf.writeInt8(2, 0); + * buf.writeInt8(-2, 1); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.0 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 1`. + * @return `offset` plus the number of bytes written. + */ + writeInt8(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as little-endian. The `value` must be a valid signed 16-bit integer. Behavior is undefined when `value` is + * anything other than a signed 16-bit integer. + * + * The `value` is interpreted and written as a two's complement signed integer. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(2); + * + * buf.writeInt16LE(0x0304, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 2`. + * @return `offset` plus the number of bytes written. + */ + writeInt16LE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as big-endian. The `value` must be a valid signed 16-bit integer. Behavior is undefined when `value` is + * anything other than a signed 16-bit integer. + * + * The `value` is interpreted and written as a two's complement signed integer. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(2); + * + * buf.writeInt16BE(0x0102, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 2`. + * @return `offset` plus the number of bytes written. + */ + writeInt16BE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as little-endian. The `value` must be a valid signed 32-bit integer. Behavior is undefined when `value` is + * anything other than a signed 32-bit integer. + * + * The `value` is interpreted and written as a two's complement signed integer. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(4); + * + * buf.writeInt32LE(0x05060708, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 4`. + * @return `offset` plus the number of bytes written. + */ + writeInt32LE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as big-endian. The `value` must be a valid signed 32-bit integer. Behavior is undefined when `value` is + * anything other than a signed 32-bit integer. + * + * The `value` is interpreted and written as a two's complement signed integer. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(4); + * + * buf.writeInt32BE(0x01020304, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.5.5 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 4`. + * @return `offset` plus the number of bytes written. + */ + writeInt32BE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as little-endian. Behavior is + * undefined when `value` is anything other than a JavaScript number. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(4); + * + * buf.writeFloatLE(0xcafebabe, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.11.15 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 4`. + * @return `offset` plus the number of bytes written. + */ + writeFloatLE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as big-endian. Behavior is + * undefined when `value` is anything other than a JavaScript number. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(4); + * + * buf.writeFloatBE(0xcafebabe, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.11.15 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 4`. + * @return `offset` plus the number of bytes written. + */ + writeFloatBE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as little-endian. The `value` must be a JavaScript number. Behavior is undefined when `value` is anything + * other than a JavaScript number. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(8); + * + * buf.writeDoubleLE(123.456, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.11.15 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 8`. + * @return `offset` plus the number of bytes written. + */ + writeDoubleLE(value: number, offset?: number): number; + /** + * Writes `value` to `buf` at the specified `offset` as big-endian. The `value` must be a JavaScript number. Behavior is undefined when `value` is anything + * other than a JavaScript number. + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(8); + * + * buf.writeDoubleBE(123.456, 0); + * + * console.log(buf); + * // Prints: + * ``` + * @since v0.11.15 + * @param value Number to be written to `buf`. + * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - 8`. + * @return `offset` plus the number of bytes written. + */ + writeDoubleBE(value: number, offset?: number): number; + /** + * Fills `buf` with the specified `value`. If the `offset` and `end` are not given, + * the entire `buf` will be filled: + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * // Fill a `Buffer` with the ASCII character 'h'. + * + * const b = Buffer.allocUnsafe(50).fill('h'); + * + * console.log(b.toString()); + * // Prints: hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh + * + * // Fill a buffer with empty string + * const c = Buffer.allocUnsafe(5).fill(''); + * + * console.log(c.fill('')); + * // Prints: + * ``` + * + * `value` is coerced to a `uint32` value if it is not a string, `Buffer`, or + * integer. If the resulting integer is greater than `255` (decimal), `buf` will be + * filled with `value & 255`. + * + * If the final write of a `fill()` operation falls on a multi-byte character, + * then only the bytes of that character that fit into `buf` are written: + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * // Fill a `Buffer` with character that takes up two bytes in UTF-8. + * + * console.log(Buffer.allocUnsafe(5).fill('\u0222')); + * // Prints: + * ``` + * + * If `value` contains invalid characters, it is truncated; if no valid + * fill data remains, an exception is thrown: + * + * ```js + * import { Buffer } from 'node:buffer'; + * + * const buf = Buffer.allocUnsafe(5); + * + * console.log(buf.fill('a')); + * // Prints: + * console.log(buf.fill('aazz', 'hex')); + * // Prints: + * console.log(buf.fill('zz', 'hex')); + * // Throws an exception. + * ``` + * @since v0.5.0 + * @param value The value with which to fill `buf`. Empty value (string, Uint8Array, Buffer) is coerced to `0`. + * @param [offset=0] Number of bytes to skip before starting to fill `buf`. + * @param [end=buf.length] Where to stop filling `buf` (not inclusive). + * @param [encoding='utf8'] The encoding for `value` if `value` is a string. + * @return A reference to `buf`. + */ + + } + + var Buffer: BufferConstructor; + } +} + +declare module "node:buffer" { + export * from "buffer"; +} diff --git a/buffer/types/package.json b/buffer/types/package.json new file mode 100644 index 0000000..081ab21 --- /dev/null +++ b/buffer/types/package.json @@ -0,0 +1,19 @@ +{ + "name": "@dop251/types-goja_nodejs-buffer", + "version": "0.0.1-rc2", + "types": "buffer.d.ts", + "scripts": { + "test": "tsc" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/dop251/goja_nodejs.git" + }, + "dependencies": { + "@dop251/types-goja_nodejs-global": "0.0.1-rc2" + }, + "devDependencies": { + "typescript": "next" + }, + "private": false +} diff --git a/buffer/types/tsconfig.json b/buffer/types/tsconfig.json new file mode 100644 index 0000000..6d09a23 --- /dev/null +++ b/buffer/types/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "esnext", + "lib": [ + "es6", + "dom" + ], + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noEmit": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/console/module.go b/console/module.go new file mode 100644 index 0000000..e96afd2 --- /dev/null +++ b/console/module.go @@ -0,0 +1,72 @@ +package console + +import ( + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/require" + "github.com/dop251/goja_nodejs/util" +) + +const ModuleName = "console" + +type Console struct { + runtime *goja.Runtime + util *goja.Object + printer Printer +} + +type Printer interface { + Log(string) + Warn(string) + Error(string) +} + +func (c *Console) log(p func(string)) func(goja.FunctionCall) goja.Value { + return func(call goja.FunctionCall) goja.Value { + if format, ok := goja.AssertFunction(c.util.Get("format")); ok { + ret, err := format(c.util, call.Arguments...) + if err != nil { + panic(err) + } + + p(ret.String()) + } else { + panic(c.runtime.NewTypeError("util.format is not a function")) + } + + return nil + } +} + +func Require(runtime *goja.Runtime, module *goja.Object) { + requireWithPrinter(defaultStdPrinter)(runtime, module) +} + +func RequireWithPrinter(printer Printer) require.ModuleLoader { + return requireWithPrinter(printer) +} + +func requireWithPrinter(printer Printer) require.ModuleLoader { + return func(runtime *goja.Runtime, module *goja.Object) { + c := &Console{ + runtime: runtime, + printer: printer, + } + + c.util = require.Require(runtime, util.ModuleName).(*goja.Object) + + o := module.Get("exports").(*goja.Object) + o.Set("log", c.log(c.printer.Log)) + o.Set("error", c.log(c.printer.Error)) + o.Set("warn", c.log(c.printer.Warn)) + o.Set("info", c.log(c.printer.Log)) + o.Set("debug", c.log(c.printer.Log)) + } +} + +func Enable(runtime *goja.Runtime) { + runtime.Set("console", require.Require(runtime, ModuleName)) +} + +func init() { + require.RegisterCoreModule(ModuleName, Require) +} diff --git a/console/module_test.go b/console/module_test.go new file mode 100644 index 0000000..9d69408 --- /dev/null +++ b/console/module_test.go @@ -0,0 +1,78 @@ +package console + +import ( + "testing" + + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/require" +) + +func TestConsole(t *testing.T) { + vm := goja.New() + + new(require.Registry).Enable(vm) + Enable(vm) + + if c := vm.Get("console"); c == nil { + t.Fatal("console not found") + } + + if _, err := vm.RunString("console.log('')"); err != nil { + t.Fatal("console.log() error", err) + } + + if _, err := vm.RunString("console.error('')"); err != nil { + t.Fatal("console.error() error", err) + } + + if _, err := vm.RunString("console.warn('')"); err != nil { + t.Fatal("console.warn() error", err) + } + + if _, err := vm.RunString("console.info('')"); err != nil { + t.Fatal("console.info() error", err) + } + + if _, err := vm.RunString("console.debug('')"); err != nil { + t.Fatal("console.debug() error", err) + } +} + +func TestConsoleWithPrinter(t *testing.T) { + var stdoutStr, stderrStr string + + printer := StdPrinter{ + StdoutPrint: func(s string) { stdoutStr += s }, + StderrPrint: func(s string) { stderrStr += s }, + } + + vm := goja.New() + + registry := new(require.Registry) + registry.Enable(vm) + registry.RegisterNativeModule(ModuleName, RequireWithPrinter(printer)) + Enable(vm) + + if c := vm.Get("console"); c == nil { + t.Fatal("console not found") + } + + _, err := vm.RunString(` + console.log('a') + console.error('b') + console.warn('c') + console.debug('d') + console.info('e') + `) + if err != nil { + t.Fatal(err) + } + + if want := "ade"; stdoutStr != want { + t.Fatalf("Unexpected stdout output: got %q, want %q", stdoutStr, want) + } + + if want := "bc"; stderrStr != want { + t.Fatalf("Unexpected stderr output: got %q, want %q", stderrStr, want) + } +} diff --git a/console/std_printer.go b/console/std_printer.go new file mode 100644 index 0000000..ffb2324 --- /dev/null +++ b/console/std_printer.go @@ -0,0 +1,38 @@ +package console + +import ( + "log" + "os" +) + +var ( + stderrLogger = log.Default() // the default logger output to stderr + stdoutLogger = log.New(os.Stdout, "", log.LstdFlags) + + defaultStdPrinter Printer = &StdPrinter{ + StdoutPrint: func(s string) { stdoutLogger.Print(s) }, + StderrPrint: func(s string) { stderrLogger.Print(s) }, + } +) + +// StdPrinter implements the console.Printer interface +// that prints to the stdout or stderr. +type StdPrinter struct { + StdoutPrint func(s string) + StderrPrint func(s string) +} + +// Log prints s to the stdout. +func (p StdPrinter) Log(s string) { + p.StdoutPrint(s) +} + +// Warn prints s to the stderr. +func (p StdPrinter) Warn(s string) { + p.StderrPrint(s) +} + +// Error prints s to the stderr. +func (p StdPrinter) Error(s string) { + p.StderrPrint(s) +} diff --git a/errors/errors.go b/errors/errors.go new file mode 100644 index 0000000..6a99035 --- /dev/null +++ b/errors/errors.go @@ -0,0 +1,97 @@ +package errors + +import ( + "fmt" + + "github.com/dop251/goja" +) + +const ( + ErrCodeInvalidArgType = "ERR_INVALID_ARG_TYPE" + ErrCodeInvalidArgValue = "ERR_INVALID_ARG_VALUE" + ErrCodeInvalidThis = "ERR_INVALID_THIS" + ErrCodeMissingArgs = "ERR_MISSING_ARGS" + ErrCodeOutOfRange = "ERR_OUT_OF_RANGE" +) + +func error_toString(call goja.FunctionCall, r *goja.Runtime) goja.Value { + this := call.This.ToObject(r) + var name, msg string + if n := this.Get("name"); n != nil && !goja.IsUndefined(n) { + name = n.String() + } else { + name = "Error" + } + if m := this.Get("message"); m != nil && !goja.IsUndefined(m) { + msg = m.String() + } + if code := this.Get("code"); code != nil && !goja.IsUndefined(code) { + if name != "" { + name += " " + } + name += "[" + code.String() + "]" + } + if msg != "" { + if name != "" { + name += ": " + } + name += msg + } + return r.ToValue(name) +} + +func addProps(r *goja.Runtime, e *goja.Object, code string) { + e.Set("code", code) + e.DefineDataProperty("toString", r.ToValue(error_toString), goja.FLAG_TRUE, goja.FLAG_TRUE, goja.FLAG_FALSE) +} + +func NewTypeError(r *goja.Runtime, code string, params ...interface{}) *goja.Object { + e := r.NewTypeError(params...) + addProps(r, e, code) + return e +} + +func NewRangeError(r *goja.Runtime, code string, params ...interface{}) *goja.Object { + ctor, _ := r.Get("RangeError").(*goja.Object) + return NewError(r, ctor, code, params...) +} + +func NewError(r *goja.Runtime, ctor *goja.Object, code string, args ...interface{}) *goja.Object { + if ctor == nil { + ctor, _ = r.Get("Error").(*goja.Object) + } + if ctor == nil { + return nil + } + msg := "" + if len(args) > 0 { + f, _ := args[0].(string) + msg = fmt.Sprintf(f, args[1:]...) + } + o, err := r.New(ctor, r.ToValue(msg)) + if err != nil { + panic(err) + } + addProps(r, o, code) + return o +} + +func NewArgumentNotBigIntTypeError(r *goja.Runtime, name string) *goja.Object { + return NewNotCorrectTypeError(r, name, "BigInt") +} + +func NewArgumentNotStringTypeError(r *goja.Runtime, name string) *goja.Object { + return NewNotCorrectTypeError(r, name, "string") +} + +func NewArgumentNotNumberTypeError(r *goja.Runtime, name string) *goja.Object { + return NewNotCorrectTypeError(r, name, "number") +} + +func NewNotCorrectTypeError(r *goja.Runtime, name, _type string) *goja.Object { + return NewTypeError(r, ErrCodeInvalidArgType, "The \"%s\" argument must be of type %s.", name, _type) +} + +func NewArgumentOutOfRangeError(r *goja.Runtime, name string, v any) *goja.Object { + return NewRangeError(r, ErrCodeOutOfRange, "The value of \"%s\" %v is out of range.", name, v) +} diff --git a/eventloop/eventloop.go b/eventloop/eventloop.go new file mode 100644 index 0000000..d047816 --- /dev/null +++ b/eventloop/eventloop.go @@ -0,0 +1,514 @@ +package eventloop + +import ( + "sync" + "sync/atomic" + "time" + + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/console" + "github.com/dop251/goja_nodejs/require" +) + +type job struct { + cancel func() bool + fn func() + idx int + + cancelled bool +} + +type Timer struct { + job + timer *time.Timer +} + +type Interval struct { + job + ticker *time.Ticker + stopChan chan struct{} +} + +type Immediate struct { + job +} + +type EventLoop struct { + vm *goja.Runtime + jobChan chan func() + jobs []*job + jobCount int32 + canRun int32 + + auxJobsLock sync.Mutex + wakeupChan chan struct{} + + auxJobsSpare, auxJobs []func() + + stopLock sync.Mutex + stopCond *sync.Cond + running bool + terminated bool + + enableConsole bool + registry *require.Registry +} + +func NewEventLoop(opts ...Option) *EventLoop { + vm := goja.New() + + loop := &EventLoop{ + vm: vm, + jobChan: make(chan func()), + wakeupChan: make(chan struct{}, 1), + enableConsole: true, + } + loop.stopCond = sync.NewCond(&loop.stopLock) + + for _, opt := range opts { + opt(loop) + } + if loop.registry == nil { + loop.registry = new(require.Registry) + } + loop.registry.Enable(vm) + if loop.enableConsole { + console.Enable(vm) + } + vm.Set("setTimeout", loop.setTimeout) + vm.Set("setInterval", loop.setInterval) + vm.Set("setImmediate", loop.setImmediate) + vm.Set("clearTimeout", loop.clearTimeout) + vm.Set("clearInterval", loop.clearInterval) + vm.Set("clearImmediate", loop.clearImmediate) + + return loop +} + +type Option func(*EventLoop) + +// EnableConsole controls whether the "console" module is loaded into +// the runtime used by the loop. By default, loops are created with +// the "console" module loaded, pass EnableConsole(false) to +// NewEventLoop to disable this behavior. +func EnableConsole(enableConsole bool) Option { + return func(loop *EventLoop) { + loop.enableConsole = enableConsole + } +} + +func WithRegistry(registry *require.Registry) Option { + return func(loop *EventLoop) { + loop.registry = registry + } +} + +func (loop *EventLoop) schedule(call goja.FunctionCall, repeating bool) goja.Value { + if fn, ok := goja.AssertFunction(call.Argument(0)); ok { + delay := call.Argument(1).ToInteger() + var args []goja.Value + if len(call.Arguments) > 2 { + args = append(args, call.Arguments[2:]...) + } + f := func() { fn(nil, args...) } + loop.jobCount++ + var job *job + var ret goja.Value + if repeating { + interval := loop.newInterval(f) + interval.start(loop, time.Duration(delay)*time.Millisecond) + job = &interval.job + ret = loop.vm.ToValue(interval) + } else { + timeout := loop.newTimeout(f) + timeout.start(loop, time.Duration(delay)*time.Millisecond) + job = &timeout.job + ret = loop.vm.ToValue(timeout) + } + job.idx = len(loop.jobs) + loop.jobs = append(loop.jobs, job) + return ret + } + return nil +} + +func (loop *EventLoop) setTimeout(call goja.FunctionCall) goja.Value { + return loop.schedule(call, false) +} + +func (loop *EventLoop) setInterval(call goja.FunctionCall) goja.Value { + return loop.schedule(call, true) +} + +func (loop *EventLoop) setImmediate(call goja.FunctionCall) goja.Value { + if fn, ok := goja.AssertFunction(call.Argument(0)); ok { + var args []goja.Value + if len(call.Arguments) > 1 { + args = append(args, call.Arguments[1:]...) + } + f := func() { fn(nil, args...) } + loop.jobCount++ + return loop.vm.ToValue(loop.addImmediate(f)) + } + return nil +} + +// SetTimeout schedules to run the specified function in the context +// of the loop as soon as possible after the specified timeout period. +// SetTimeout returns a Timer which can be passed to ClearTimeout. +// The instance of goja.Runtime that is passed to the function and any Values derived +// from it must not be used outside the function. SetTimeout is +// safe to call inside or outside the loop. +// If the loop is terminated (see Terminate()) returns nil. +func (loop *EventLoop) SetTimeout(fn func(*goja.Runtime), timeout time.Duration) *Timer { + t := loop.newTimeout(func() { fn(loop.vm) }) + if loop.addAuxJob(func() { + t.start(loop, timeout) + loop.jobCount++ + t.idx = len(loop.jobs) + loop.jobs = append(loop.jobs, &t.job) + }) { + return t + } + return nil +} + +// ClearTimeout cancels a Timer returned by SetTimeout if it has not run yet. +// ClearTimeout is safe to call inside or outside the loop. +func (loop *EventLoop) ClearTimeout(t *Timer) { + loop.addAuxJob(func() { + loop.clearTimeout(t) + }) +} + +// SetInterval schedules to repeatedly run the specified function in +// the context of the loop as soon as possible after every specified +// timeout period. SetInterval returns an Interval which can be +// passed to ClearInterval. The instance of goja.Runtime that is passed to the +// function and any Values derived from it must not be used outside +// the function. SetInterval is safe to call inside or outside the +// loop. +// If the loop is terminated (see Terminate()) returns nil. +func (loop *EventLoop) SetInterval(fn func(*goja.Runtime), timeout time.Duration) *Interval { + i := loop.newInterval(func() { fn(loop.vm) }) + if loop.addAuxJob(func() { + i.start(loop, timeout) + loop.jobCount++ + i.idx = len(loop.jobs) + loop.jobs = append(loop.jobs, &i.job) + }) { + return i + } + return nil +} + +// ClearInterval cancels an Interval returned by SetInterval. +// ClearInterval is safe to call inside or outside the loop. +func (loop *EventLoop) ClearInterval(i *Interval) { + loop.addAuxJob(func() { + loop.clearInterval(i) + }) +} + +func (loop *EventLoop) setRunning() { + loop.stopLock.Lock() + defer loop.stopLock.Unlock() + if loop.running { + panic("Loop is already started") + } + loop.running = true + atomic.StoreInt32(&loop.canRun, 1) + loop.auxJobsLock.Lock() + loop.terminated = false + loop.auxJobsLock.Unlock() +} + +// Run calls the specified function, starts the event loop and waits until there are no more delayed jobs to run +// after which it stops the loop and returns. +// The instance of goja.Runtime that is passed to the function and any Values derived from it must not be used +// outside the function. +// Do NOT use this function while the loop is already running. Use RunOnLoop() instead. +// If the loop is already started it will panic. +func (loop *EventLoop) Run(fn func(*goja.Runtime)) { + loop.setRunning() + fn(loop.vm) + loop.run(false) +} + +// Start the event loop in the background. The loop continues to run until Stop() is called. +// If the loop is already started it will panic. +func (loop *EventLoop) Start() { + loop.setRunning() + go loop.run(true) +} + +// StartInForeground starts the event loop in the current goroutine. The loop continues to run until Stop() is called. +// If the loop is already started it will panic. +// Use this instead of Start if you want to recover from panics that may occur while calling native Go functions from +// within setInterval and setTimeout callbacks. +func (loop *EventLoop) StartInForeground() { + loop.setRunning() + loop.run(true) +} + +// Stop the loop that was started with Start(). After this function returns there will be no more jobs executed +// by the loop. It is possible to call Start() or Run() again after this to resume the execution. +// Note, it does not cancel active timeouts (use Terminate() instead if you want this). +// It is not allowed to run Start() (or Run()) and Stop() or Terminate() concurrently. +// Calling Stop() on a non-running loop has no effect. +// It is not allowed to call Stop() from the loop, because it is synchronous and cannot complete until the loop +// is not running any jobs. Use StopNoWait() instead. +// return number of jobs remaining +func (loop *EventLoop) Stop() int { + loop.stopLock.Lock() + for loop.running { + atomic.StoreInt32(&loop.canRun, 0) + loop.wakeup() + loop.stopCond.Wait() + } + loop.stopLock.Unlock() + return int(loop.jobCount) +} + +// StopNoWait tells the loop to stop and returns immediately. Can be used inside the loop. Calling it on a +// non-running loop has no effect. +func (loop *EventLoop) StopNoWait() { + loop.stopLock.Lock() + if loop.running { + atomic.StoreInt32(&loop.canRun, 0) + loop.wakeup() + } + loop.stopLock.Unlock() +} + +// Terminate stops the loop and clears all active timeouts and intervals. After it returns there are no +// active timers or goroutines associated with the loop. Any attempt to submit a task (by using RunOnLoop(), +// SetTimeout() or SetInterval()) will not succeed. +// After being terminated the loop can be restarted again by using Start() or Run(). +// This method must not be called concurrently with Stop*(), Start(), or Run(). +func (loop *EventLoop) Terminate() { + loop.Stop() + + loop.auxJobsLock.Lock() + loop.terminated = true + loop.auxJobsLock.Unlock() + + loop.runAux() + + for i := 0; i < len(loop.jobs); i++ { + job := loop.jobs[i] + if !job.cancelled { + job.cancelled = true + if job.cancel() { + loop.removeJob(job) + i-- + } + } + } + + for len(loop.jobs) > 0 { + (<-loop.jobChan)() + } +} + +// RunOnLoop schedules to run the specified function in the context of the loop as soon as possible. +// The order of the runs is preserved (i.e. the functions will be called in the same order as calls to RunOnLoop()) +// The instance of goja.Runtime that is passed to the function and any Values derived from it must not be used +// outside the function. It is safe to call inside or outside the loop. +// Returns true on success or false if the loop is terminated (see Terminate()). +func (loop *EventLoop) RunOnLoop(fn func(*goja.Runtime)) bool { + return loop.addAuxJob(func() { fn(loop.vm) }) +} + +func (loop *EventLoop) runAux() { + loop.auxJobsLock.Lock() + jobs := loop.auxJobs + loop.auxJobs = loop.auxJobsSpare + loop.auxJobsLock.Unlock() + for i, job := range jobs { + job() + jobs[i] = nil + } + loop.auxJobsSpare = jobs[:0] +} + +func (loop *EventLoop) run(inBackground bool) { + loop.runAux() + if inBackground { + loop.jobCount++ + } +LOOP: + for loop.jobCount > 0 { + select { + case job := <-loop.jobChan: + job() + case <-loop.wakeupChan: + loop.runAux() + if atomic.LoadInt32(&loop.canRun) == 0 { + break LOOP + } + } + } + if inBackground { + loop.jobCount-- + } + + loop.stopLock.Lock() + loop.running = false + loop.stopLock.Unlock() + loop.stopCond.Broadcast() +} + +func (loop *EventLoop) wakeup() { + select { + case loop.wakeupChan <- struct{}{}: + default: + } +} + +func (loop *EventLoop) addAuxJob(fn func()) bool { + loop.auxJobsLock.Lock() + if loop.terminated { + loop.auxJobsLock.Unlock() + return false + } + loop.auxJobs = append(loop.auxJobs, fn) + loop.auxJobsLock.Unlock() + loop.wakeup() + return true +} + +func (loop *EventLoop) newTimeout(f func()) *Timer { + t := &Timer{ + job: job{fn: f}, + } + t.cancel = t.doCancel + + return t +} + +func (t *Timer) start(loop *EventLoop, timeout time.Duration) { + t.timer = time.AfterFunc(timeout, func() { + loop.jobChan <- func() { + loop.doTimeout(t) + } + }) +} + +func (loop *EventLoop) newInterval(f func()) *Interval { + i := &Interval{ + job: job{fn: f}, + stopChan: make(chan struct{}), + } + i.cancel = i.doCancel + + return i +} + +func (i *Interval) start(loop *EventLoop, timeout time.Duration) { + // https://nodejs.org/api/timers.html#timers_setinterval_callback_delay_args + if timeout <= 0 { + timeout = time.Millisecond + } + i.ticker = time.NewTicker(timeout) + go i.run(loop) +} + +func (loop *EventLoop) addImmediate(f func()) *Immediate { + i := &Immediate{ + job: job{fn: f}, + } + loop.addAuxJob(func() { + loop.doImmediate(i) + }) + return i +} + +func (loop *EventLoop) doTimeout(t *Timer) { + loop.removeJob(&t.job) + if !t.cancelled { + t.cancelled = true + loop.jobCount-- + t.fn() + } +} + +func (loop *EventLoop) doInterval(i *Interval) { + if !i.cancelled { + i.fn() + } +} + +func (loop *EventLoop) doImmediate(i *Immediate) { + if !i.cancelled { + i.cancelled = true + loop.jobCount-- + i.fn() + } +} + +func (loop *EventLoop) clearTimeout(t *Timer) { + if t != nil && !t.cancelled { + t.cancelled = true + loop.jobCount-- + if t.doCancel() { + loop.removeJob(&t.job) + } + } +} + +func (loop *EventLoop) clearInterval(i *Interval) { + if i != nil && !i.cancelled { + i.cancelled = true + loop.jobCount-- + i.doCancel() + } +} + +func (loop *EventLoop) removeJob(job *job) { + idx := job.idx + if idx < 0 { + return + } + if idx < len(loop.jobs)-1 { + loop.jobs[idx] = loop.jobs[len(loop.jobs)-1] + loop.jobs[idx].idx = idx + } + loop.jobs[len(loop.jobs)-1] = nil + loop.jobs = loop.jobs[:len(loop.jobs)-1] + job.idx = -1 +} + +func (loop *EventLoop) clearImmediate(i *Immediate) { + if i != nil && !i.cancelled { + i.cancelled = true + loop.jobCount-- + } +} + +func (i *Interval) doCancel() bool { + close(i.stopChan) + return false +} + +func (t *Timer) doCancel() bool { + return t.timer.Stop() +} + +func (i *Interval) run(loop *EventLoop) { +L: + for { + select { + case <-i.stopChan: + i.ticker.Stop() + break L + case <-i.ticker.C: + loop.jobChan <- func() { + loop.doInterval(i) + } + } + } + loop.jobChan <- func() { + loop.removeJob(&i.job) + } +} diff --git a/eventloop/eventloop_test.go b/eventloop/eventloop_test.go new file mode 100644 index 0000000..5d2af73 --- /dev/null +++ b/eventloop/eventloop_test.go @@ -0,0 +1,641 @@ +package eventloop + +import ( + "fmt" + "sync/atomic" + "testing" + "time" + + "github.com/dop251/goja" + + "go.uber.org/goleak" +) + +func TestRun(t *testing.T) { + t.Parallel() + const SCRIPT = ` + var calledAt; + setTimeout(function() { + calledAt = now(); + }, 1000); + ` + + loop := NewEventLoop() + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + startTime := time.Now() + loop.Run(func(vm *goja.Runtime) { + vm.Set("now", time.Now) + _, err = vm.RunProgram(prg) + }) + if err != nil { + t.Fatal(err) + } + var calledAt time.Time + loop.Run(func(vm *goja.Runtime) { + err = vm.ExportTo(vm.Get("calledAt"), &calledAt) + }) + if err != nil { + t.Fatal(err) + } + if calledAt.IsZero() { + t.Fatal("Not called") + } + if dur := calledAt.Sub(startTime); dur < time.Second { + t.Fatal(dur) + } +} + +func TestStart(t *testing.T) { + t.Parallel() + const SCRIPT = ` + var calledAt; + setTimeout(function() { + calledAt = now(); + }, 1000); + ` + + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + + loop := NewEventLoop() + startTime := time.Now() + loop.Start() + + loop.RunOnLoop(func(vm *goja.Runtime) { + vm.Set("now", time.Now) + vm.RunProgram(prg) + }) + + time.Sleep(2 * time.Second) + if remainingJobs := loop.Stop(); remainingJobs != 0 { + t.Fatal(remainingJobs) + } + + var calledAt time.Time + loop.Run(func(vm *goja.Runtime) { + err = vm.ExportTo(vm.Get("calledAt"), &calledAt) + }) + if err != nil { + t.Fatal(err) + } + if calledAt.IsZero() { + t.Fatal("Not called") + } + if dur := calledAt.Sub(startTime); dur < time.Second { + t.Fatal(dur) + } +} + +func TestStartInForeground(t *testing.T) { + t.Parallel() + const SCRIPT = ` + var calledAt; + setTimeout(function() { + calledAt = now(); + }, 1000); + ` + + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + + loop := NewEventLoop() + startTime := time.Now() + go loop.StartInForeground() + + loop.RunOnLoop(func(vm *goja.Runtime) { + vm.Set("now", time.Now) + vm.RunProgram(prg) + }) + + time.Sleep(2 * time.Second) + if remainingJobs := loop.Stop(); remainingJobs != 0 { + t.Fatal(remainingJobs) + } + + var calledAt time.Time + loop.Run(func(vm *goja.Runtime) { + err = vm.ExportTo(vm.Get("calledAt"), &calledAt) + }) + if err != nil { + t.Fatal(err) + } + if calledAt.IsZero() { + t.Fatal("Not called") + } + if dur := calledAt.Sub(startTime); dur < time.Second { + t.Fatal(dur) + } +} + +func TestInterval(t *testing.T) { + t.Parallel() + const SCRIPT = ` + var count = 0; + var t = setInterval(function(times) { + console.log("tick"); + if (++count > times) { + clearInterval(t); + } + }, 1000, 2); + console.log("Started"); + ` + + loop := NewEventLoop() + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + + loop.Run(func(vm *goja.Runtime) { + _, err = vm.RunProgram(prg) + }) + if err != nil { + t.Fatal(err) + } + + var count int64 + loop.Run(func(vm *goja.Runtime) { + count = vm.Get("count").ToInteger() + }) + if count != 3 { + t.Fatal(count) + } +} + +func TestImmediate(t *testing.T) { + t.Parallel() + const SCRIPT = ` + let log = []; + function cb(arg) { + log.push(arg); + } + var i; + var t = setImmediate(function() { + cb("tick"); + setImmediate(cb, "tick 2"); + i = setImmediate(cb, "should not run") + }); + setImmediate(function() { + clearImmediate(i); + }); + cb("Started"); + ` + + loop := NewEventLoop() + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + loop.Run(func(vm *goja.Runtime) { + _, err = vm.RunProgram(prg) + }) + if err != nil { + t.Fatal(err) + } + + loop.Run(func(vm *goja.Runtime) { + _, err = vm.RunString(` + if (log.length != 3) { + throw new Error("Invalid log length: " + log); + } + if (log[0] !== "Started" || log[1] !== "tick" || log[2] !== "tick 2") { + throw new Error("Invalid log: " + log); + } + `) + }) + if err != nil { + t.Fatal(err) + } +} + +func TestRunNoSchedule(t *testing.T) { + loop := NewEventLoop() + fired := false + loop.Run(func(vm *goja.Runtime) { // should not hang + fired = true + // do not schedule anything + }) + + if !fired { + t.Fatal("Not fired") + } +} + +func TestRunWithConsole(t *testing.T) { + const SCRIPT = ` + console.log("Started"); + ` + + loop := NewEventLoop() + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + loop.Run(func(vm *goja.Runtime) { + _, err = vm.RunProgram(prg) + }) + if err != nil { + t.Fatal("Call to console.log generated an error", err) + } + + loop = NewEventLoop(EnableConsole(true)) + prg, err = goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + loop.Run(func(vm *goja.Runtime) { + _, err = vm.RunProgram(prg) + }) + if err != nil { + t.Fatal("Call to console.log generated an error", err) + } +} + +func TestRunNoConsole(t *testing.T) { + const SCRIPT = ` + console.log("Started"); + ` + + loop := NewEventLoop(EnableConsole(false)) + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + loop.Run(func(vm *goja.Runtime) { + _, err = vm.RunProgram(prg) + }) + if err == nil { + t.Fatal("Call to console.log did not generate an error", err) + } +} + +func TestClearIntervalRace(t *testing.T) { + t.Parallel() + const SCRIPT = ` + console.log("calling setInterval"); + var t = setInterval(function() { + console.log("tick"); + }, 500); + console.log("calling sleep"); + sleep(2000); + console.log("calling clearInterval"); + clearInterval(t); + ` + + loop := NewEventLoop() + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + // Should not hang + loop.Run(func(vm *goja.Runtime) { + vm.Set("sleep", func(ms int) { + <-time.After(time.Duration(ms) * time.Millisecond) + }) + vm.RunProgram(prg) + }) +} + +func TestNativeTimeout(t *testing.T) { + t.Parallel() + fired := false + loop := NewEventLoop() + loop.SetTimeout(func(*goja.Runtime) { + fired = true + }, 1*time.Second) + loop.Run(func(*goja.Runtime) { + // do not schedule anything + }) + if !fired { + t.Fatal("Not fired") + } +} + +func TestNativeClearTimeout(t *testing.T) { + t.Parallel() + fired := false + loop := NewEventLoop() + timer := loop.SetTimeout(func(*goja.Runtime) { + fired = true + }, 2*time.Second) + loop.SetTimeout(func(*goja.Runtime) { + loop.ClearTimeout(timer) + }, 1*time.Second) + loop.Run(func(*goja.Runtime) { + // do not schedule anything + }) + if fired { + t.Fatal("Cancelled timer fired!") + } +} + +func TestNativeInterval(t *testing.T) { + t.Parallel() + count := 0 + loop := NewEventLoop() + var i *Interval + i = loop.SetInterval(func(*goja.Runtime) { + t.Log("tick") + count++ + if count > 2 { + loop.ClearInterval(i) + } + }, 1*time.Second) + loop.Run(func(*goja.Runtime) { + // do not schedule anything + }) + if count != 3 { + t.Fatal("Expected interval to fire 3 times, got", count) + } +} + +func TestNativeClearInterval(t *testing.T) { + t.Parallel() + count := 0 + loop := NewEventLoop() + loop.Run(func(*goja.Runtime) { + i := loop.SetInterval(func(*goja.Runtime) { + t.Log("tick") + count++ + }, 500*time.Millisecond) + <-time.After(2 * time.Second) + loop.ClearInterval(i) + }) + if count != 0 { + t.Fatal("Expected interval to fire 0 times, got", count) + } +} + +func TestSetAndClearOnStoppedLoop(t *testing.T) { + t.Parallel() + loop := NewEventLoop() + timeout := loop.SetTimeout(func(runtime *goja.Runtime) { + panic("must not run") + }, 1*time.Millisecond) + loop.ClearTimeout(timeout) + loop.Start() + time.Sleep(10 * time.Millisecond) + loop.Terminate() +} + +func TestSetTimeoutConcurrent(t *testing.T) { + t.Parallel() + loop := NewEventLoop() + loop.Start() + ch := make(chan struct{}, 1) + loop.SetTimeout(func(*goja.Runtime) { + ch <- struct{}{} + }, 100*time.Millisecond) + <-ch + loop.Stop() +} + +func TestClearTimeoutConcurrent(t *testing.T) { + t.Parallel() + loop := NewEventLoop() + loop.Start() + timer := loop.SetTimeout(func(*goja.Runtime) { + }, 100*time.Millisecond) + loop.ClearTimeout(timer) + loop.Stop() + if c := loop.jobCount; c != 0 { + t.Fatalf("jobCount: %d", c) + } +} + +func TestClearIntervalConcurrent(t *testing.T) { + t.Parallel() + loop := NewEventLoop() + loop.Start() + ch := make(chan struct{}, 1) + i := loop.SetInterval(func(*goja.Runtime) { + ch <- struct{}{} + }, 500*time.Millisecond) + + <-ch + loop.ClearInterval(i) + loop.Stop() + if c := loop.jobCount; c != 0 { + t.Fatalf("jobCount: %d", c) + } +} + +func TestRunOnStoppedLoop(t *testing.T) { + t.Parallel() + loop := NewEventLoop() + var failed int32 + done := make(chan struct{}) + go func() { + for atomic.LoadInt32(&failed) == 0 { + loop.Start() + time.Sleep(10 * time.Millisecond) + loop.Stop() + } + }() + go func() { + for atomic.LoadInt32(&failed) == 0 { + loop.RunOnLoop(func(*goja.Runtime) { + if !loop.running { + atomic.StoreInt32(&failed, 1) + close(done) + return + } + }) + time.Sleep(10 * time.Millisecond) + } + }() + select { + case <-done: + case <-time.After(5 * time.Second): + } + if atomic.LoadInt32(&failed) != 0 { + t.Fatal("running job on stopped loop") + } +} + +func TestPromise(t *testing.T) { + t.Parallel() + const SCRIPT = ` + let result; + const p = new Promise((resolve, reject) => { + setTimeout(() => {resolve("passed")}, 500); + }); + p.then(value => { + result = value; + }); + ` + + loop := NewEventLoop() + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + loop.Run(func(vm *goja.Runtime) { + _, err = vm.RunProgram(prg) + }) + if err != nil { + t.Fatal(err) + } + loop.Run(func(vm *goja.Runtime) { + result := vm.Get("result") + if !result.SameAs(vm.ToValue("passed")) { + err = fmt.Errorf("unexpected result: %v", result) + } + }) + if err != nil { + t.Fatal(err) + } +} + +func TestPromiseNative(t *testing.T) { + t.Parallel() + const SCRIPT = ` + let result; + p.then(value => { + result = value; + done(); + }); + ` + + loop := NewEventLoop() + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + ch := make(chan error) + loop.Start() + defer loop.Stop() + + loop.RunOnLoop(func(vm *goja.Runtime) { + vm.Set("done", func() { + ch <- nil + }) + p, resolve, _ := vm.NewPromise() + vm.Set("p", p) + _, err = vm.RunProgram(prg) + if err != nil { + ch <- err + return + } + go func() { + time.Sleep(500 * time.Millisecond) + loop.RunOnLoop(func(*goja.Runtime) { + resolve("passed") + }) + }() + }) + err = <-ch + if err != nil { + t.Fatal(err) + } + loop.RunOnLoop(func(vm *goja.Runtime) { + result := vm.Get("result") + if !result.SameAs(vm.ToValue("passed")) { + ch <- fmt.Errorf("unexpected result: %v", result) + } else { + ch <- nil + } + }) + err = <-ch + if err != nil { + t.Fatal(err) + } +} + +func TestEventLoop_StopNoWait(t *testing.T) { + t.Parallel() + loop := NewEventLoop() + var ran int32 + loop.Run(func(runtime *goja.Runtime) { + loop.SetTimeout(func(*goja.Runtime) { + atomic.StoreInt32(&ran, 1) + }, 5*time.Second) + + loop.SetTimeout(func(*goja.Runtime) { + loop.StopNoWait() + }, 500*time.Millisecond) + }) + + if atomic.LoadInt32(&ran) != 0 { + t.Fatal("ran != 0") + } +} + +func TestEventLoop_ClearRunningTimeout(t *testing.T) { + t.Parallel() + const SCRIPT = ` + var called = 0; + let aTimer; + function a() { + if (++called > 5) { + return; + } + if (aTimer) { + clearTimeout(aTimer); + } + console.log("ok"); + aTimer = setTimeout(a, 500); + } + a();` + + prg, err := goja.Compile("main.js", SCRIPT, false) + if err != nil { + t.Fatal(err) + } + + loop := NewEventLoop() + + loop.Run(func(vm *goja.Runtime) { + _, err = vm.RunProgram(prg) + }) + + if err != nil { + t.Fatal(err) + } + + var called int64 + loop.Run(func(vm *goja.Runtime) { + called = vm.Get("called").ToInteger() + }) + if called != 6 { + t.Fatal(called) + } +} + +func TestEventLoop_Terminate(t *testing.T) { + defer goleak.VerifyNone(t) + + loop := NewEventLoop() + loop.Start() + interval := loop.SetInterval(func(vm *goja.Runtime) {}, 10*time.Millisecond) + time.Sleep(500 * time.Millisecond) + loop.ClearInterval(interval) + loop.Terminate() + + if loop.SetTimeout(func(*goja.Runtime) {}, time.Millisecond) != nil { + t.Fatal("was able to SetTimeout()") + } + if loop.SetInterval(func(*goja.Runtime) {}, time.Millisecond) != nil { + t.Fatal("was able to SetInterval()") + } + if loop.RunOnLoop(func(*goja.Runtime) {}) { + t.Fatal("was able to RunOnLoop()") + } + + ch := make(chan struct{}) + loop.Start() + if !loop.RunOnLoop(func(runtime *goja.Runtime) { + close(ch) + }) { + t.Fatal("RunOnLoop() has failed after restart") + } + <-ch + loop.Terminate() +} diff --git a/global-types/README.md b/global-types/README.md new file mode 100644 index 0000000..0fc0ddf --- /dev/null +++ b/global-types/README.md @@ -0,0 +1,3 @@ +## Core type definitions for goja_nodejs. + +This package is used by other type definition packages for goja_nodejs. You probably do not need to install it directly. diff --git a/global-types/globals.d.ts b/global-types/globals.d.ts new file mode 100644 index 0000000..ad7d80e --- /dev/null +++ b/global-types/globals.d.ts @@ -0,0 +1,16 @@ +export {}; + +declare global { + namespace GojaNodeJS { + interface Iterator extends IteratorObject { + [Symbol.iterator](): GojaNodeJS.Iterator; + } + + // Polyfill for TS 5.6's instrinsic BuiltinIteratorReturn type, required for DOM-compatible iterators + type BuiltinIteratorReturn = ReturnType extends + globalThis.Iterator ? TReturn + : any; + + } +} + diff --git a/global-types/package.json b/global-types/package.json new file mode 100644 index 0000000..e24dd7d --- /dev/null +++ b/global-types/package.json @@ -0,0 +1,16 @@ +{ + "name": "@dop251/types-goja_nodejs-global", + "version": "0.0.1-rc2", + "types": "globals.d.ts", + "scripts": { + "test": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "next" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/dop251/goja_nodejs.git" + }, + "private": false +} diff --git a/global-types/tsconfig.json b/global-types/tsconfig.json new file mode 100644 index 0000000..4c79eb1 --- /dev/null +++ b/global-types/tsconfig.json @@ -0,0 +1,18 @@ +{ + "files": ["globals.d.ts"], + "compilerOptions": { + "module": "commonjs", + "target": "esnext", + "lib": [ + "es6", + "dom" + ], + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..082cc39 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/dop251/goja_nodejs + +go 1.20 + +require ( + github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217 + github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c + go.uber.org/goleak v1.3.0 + golang.org/x/net v0.27.0 + golang.org/x/text v0.16.0 +) + +require ( + github.com/dlclark/regexp2 v1.11.4 // indirect + github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect + github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e5fa9e7 --- /dev/null +++ b/go.sum @@ -0,0 +1,22 @@ +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217 h1:16iT9CBDOniJwFGPI41MbUDfEk74hFaKTqudrX8kenY= +github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217/go.mod h1:eIb+f24U+eWQCIsj9D/ah+MD9UP+wdxuqzsdLD+mhGM= +github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c h1:mxWGS0YyquJ/ikZOjSrRjjFIbUqIP9ojyYQ+QZTU3Rg= +github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= +github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= +github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/goutil/argtypes.go b/goutil/argtypes.go new file mode 100644 index 0000000..90293ea --- /dev/null +++ b/goutil/argtypes.go @@ -0,0 +1,84 @@ +package goutil + +import ( + "math/big" + + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/errors" +) + +func RequiredIntegerArgument(r *goja.Runtime, call goja.FunctionCall, name string, argIndex int) int64 { + arg := call.Argument(argIndex) + if goja.IsNumber(arg) { + return arg.ToInteger() + } + if goja.IsUndefined(arg) { + panic(errors.NewTypeError(r, errors.ErrCodeInvalidArgType, "The \"%s\" argument is required.", name)) + } + + panic(errors.NewArgumentNotNumberTypeError(r, name)) +} + +func RequiredFloatArgument(r *goja.Runtime, call goja.FunctionCall, name string, argIndex int) float64 { + arg := call.Argument(argIndex) + if goja.IsNumber(arg) { + return arg.ToFloat() + } + if goja.IsUndefined(arg) { + panic(errors.NewTypeError(r, errors.ErrCodeInvalidArgType, "The \"%s\" argument is required.", name)) + } + + panic(errors.NewArgumentNotNumberTypeError(r, name)) +} + +func CoercedIntegerArgument(call goja.FunctionCall, argIndex int, defaultValue int64, typeMistMatchValue int64) int64 { + arg := call.Argument(argIndex) + if goja.IsNumber(arg) { + return arg.ToInteger() + } + if goja.IsUndefined(arg) { + return defaultValue + } + + return typeMistMatchValue +} + +func OptionalIntegerArgument(r *goja.Runtime, call goja.FunctionCall, name string, argIndex int, defaultValue int64) int64 { + arg := call.Argument(argIndex) + if goja.IsNumber(arg) { + return arg.ToInteger() + } + if goja.IsUndefined(arg) { + return defaultValue + } + + panic(errors.NewArgumentNotNumberTypeError(r, name)) +} + +func RequiredBigIntArgument(r *goja.Runtime, call goja.FunctionCall, name string, argIndex int) *big.Int { + arg := call.Argument(argIndex) + if goja.IsUndefined(arg) { + panic(errors.NewTypeError(r, errors.ErrCodeInvalidArgType, "The \"%s\" argument is required.", name)) + } + if !goja.IsBigInt(arg) { + panic(errors.NewArgumentNotBigIntTypeError(r, name)) + } + + n, _ := arg.Export().(*big.Int) + if n == nil { + n = new(big.Int) + } + return n +} + +func RequiredStringArgument(r *goja.Runtime, call goja.FunctionCall, name string, argIndex int) string { + arg := call.Argument(argIndex) + if goja.IsString(arg) { + return arg.String() + } + if goja.IsUndefined(arg) { + panic(errors.NewTypeError(r, errors.ErrCodeInvalidArgType, "The \"%s\" argument is required.", name)) + } + + panic(errors.NewArgumentNotStringTypeError(r, name)) +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6bf1600 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,67 @@ +{ + "name": "goja_nodejs", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "workspaces": [ + "global-types", + "url/types", + "buffer/types" + ] + }, + "buffer/types": { + "name": "@dop251/types-goja_nodejs-buffer", + "version": "0.0.1-rc2", + "dependencies": { + "@dop251/types-goja_nodejs-global": "0.0.1-rc2" + }, + "devDependencies": { + "typescript": "next" + } + }, + "global-types": { + "name": "@dop251/types-goja_nodejs-global", + "version": "0.0.1-rc2", + "devDependencies": { + "typescript": "next" + } + }, + "node_modules/@dop251/types-goja_nodejs-buffer": { + "resolved": "buffer/types", + "link": true + }, + "node_modules/@dop251/types-goja_nodejs-global": { + "resolved": "global-types", + "link": true + }, + "node_modules/@dop251/types-goja_nodejs-url": { + "resolved": "url/types", + "link": true + }, + "node_modules/typescript": { + "version": "5.9.0-dev.20250314", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.0-dev.20250314.tgz", + "integrity": "sha512-b9eLo5FjlR0BRMsYIxZYCrtTTUu97N1bh+DpQFCEm5OfRGzUg/Oc09fgct4jA4NF7R5Yg9oxWqVT90uto1TsvA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "url/types": { + "name": "@dop251/types-goja_nodejs-url", + "version": "0.0.1-rc2", + "dependencies": { + "@dop251/types-goja_nodejs-global": "0.0.1-rc2" + }, + "devDependencies": { + "typescript": "next" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..34cdbb4 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "private": true, + "workspaces": [ + "global-types", + "url/types", + "buffer/types" + ] +} diff --git a/process/module.go b/process/module.go new file mode 100644 index 0000000..28814b4 --- /dev/null +++ b/process/module.go @@ -0,0 +1,37 @@ +package process + +import ( + "os" + "strings" + + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/require" +) + +const ModuleName = "process" + +type Process struct { + env map[string]string +} + +func Require(runtime *goja.Runtime, module *goja.Object) { + p := &Process{ + env: make(map[string]string), + } + + for _, e := range os.Environ() { + envKeyValue := strings.SplitN(e, "=", 2) + p.env[envKeyValue[0]] = envKeyValue[1] + } + + o := module.Get("exports").(*goja.Object) + o.Set("env", p.env) +} + +func Enable(runtime *goja.Runtime) { + runtime.Set("process", require.Require(runtime, ModuleName)) +} + +func init() { + require.RegisterCoreModule(ModuleName, Require) +} diff --git a/process/module_test.go b/process/module_test.go new file mode 100644 index 0000000..4c6ceef --- /dev/null +++ b/process/module_test.go @@ -0,0 +1,68 @@ +package process + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/require" +) + +func TestProcessEnvStructure(t *testing.T) { + vm := goja.New() + + new(require.Registry).Enable(vm) + Enable(vm) + + if c := vm.Get("process"); c == nil { + t.Fatal("process not found") + } + + if c, err := vm.RunString("process.env"); c == nil || err != nil { + t.Fatal("error accessing process.env") + } +} + +func TestProcessEnvValuesArtificial(t *testing.T) { + os.Setenv("GOJA_IS_AWESOME", "true") + defer os.Unsetenv("GOJA_IS_AWESOME") + + vm := goja.New() + + new(require.Registry).Enable(vm) + Enable(vm) + + jsRes, err := vm.RunString("process.env['GOJA_IS_AWESOME']") + + if err != nil { + t.Fatalf("Error executing: %s", err) + } + + if jsRes.String() != "true" { + t.Fatalf("Error executing: got %s but expected %s", jsRes, "true") + } +} + +func TestProcessEnvValuesBrackets(t *testing.T) { + vm := goja.New() + + new(require.Registry).Enable(vm) + Enable(vm) + + for _, e := range os.Environ() { + envKeyValue := strings.SplitN(e, "=", 2) + jsExpr := fmt.Sprintf("process.env['%s']", envKeyValue[0]) + + jsRes, err := vm.RunString(jsExpr) + + if err != nil { + t.Fatalf("Error executing %s: %s", jsExpr, err) + } + + if jsRes.String() != envKeyValue[1] { + t.Fatalf("Error executing %s: got %s but expected %s", jsExpr, jsRes, envKeyValue[1]) + } + } +} diff --git a/require/module.go b/require/module.go new file mode 100644 index 0000000..1351e82 --- /dev/null +++ b/require/module.go @@ -0,0 +1,276 @@ +package require + +import ( + "errors" + "io" + "io/fs" + "os" + "path" + "path/filepath" + "runtime" + "sync" + "syscall" + "text/template" + + js "github.com/dop251/goja" + "github.com/dop251/goja/parser" +) + +type ModuleLoader func(*js.Runtime, *js.Object) + +// SourceLoader represents a function that returns a file data at a given path. +// The function should return ModuleFileDoesNotExistError if the file either doesn't exist or is a directory. +// This error will be ignored by the resolver and the search will continue. Any other errors will be propagated. +type SourceLoader func(path string) ([]byte, error) + +// PathResolver is a function that should return a canonical path of the path parameter relative to the base. The base +// is expected to be already canonical as it would be a result of a previous call to the PathResolver for all cases +// except for the initial evaluation, but it's a responsibility of the caller to ensure that the name of the script +// is a canonical path. To match Node JS behaviour, it should resolve symlinks. +// The path parameter is the argument of the require() call. The returned value will be supplied to the SourceLoader. +type PathResolver func(base, path string) string + +var ( + InvalidModuleError = errors.New("Invalid module") + IllegalModuleNameError = errors.New("Illegal module name") + NoSuchBuiltInModuleError = errors.New("No such built-in module") + ModuleFileDoesNotExistError = errors.New("module file does not exist") +) + +var native, builtin map[string]ModuleLoader + +// Registry contains a cache of compiled modules which can be used by multiple Runtimes +type Registry struct { + sync.Mutex + native map[string]ModuleLoader + compiled map[string]*js.Program + + srcLoader SourceLoader + pathResolver PathResolver + globalFolders []string +} + +type RequireModule struct { + r *Registry + runtime *js.Runtime + modules map[string]*js.Object + nodeModules map[string]*js.Object +} + +func NewRegistry(opts ...Option) *Registry { + r := &Registry{} + + for _, opt := range opts { + opt(r) + } + + return r +} + +func NewRegistryWithLoader(srcLoader SourceLoader) *Registry { + return NewRegistry(WithLoader(srcLoader)) +} + +type Option func(*Registry) + +// WithLoader sets a function which will be called by the require() function in order to get a source code for a +// module at the given path. The same function will be used to get external source maps. +// Note, this only affects the modules loaded by the require() function. If you need to use it as a source map +// loader for code parsed in a different way (such as runtime.RunString() or eval()), use (*Runtime).SetParserOptions() +func WithLoader(srcLoader SourceLoader) Option { + return func(r *Registry) { + r.srcLoader = srcLoader + } +} + +// WithPathResolver sets a function which will be used to resolve paths (see PathResolver). If not specified, the +// DefaultPathResolver is used. +func WithPathResolver(pathResolver PathResolver) Option { + return func(r *Registry) { + r.pathResolver = pathResolver + } +} + +// WithGlobalFolders appends the given paths to the registry's list of +// global folders to search if the requested module is not found +// elsewhere. By default, a registry's global folders list is empty. +// In the reference Node.js implementation, the default global folders +// list is $NODE_PATH, $HOME/.node_modules, $HOME/.node_libraries and +// $PREFIX/lib/node, see +// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders. +func WithGlobalFolders(globalFolders ...string) Option { + return func(r *Registry) { + r.globalFolders = globalFolders + } +} + +// Enable adds the require() function to the specified runtime. +func (r *Registry) Enable(runtime *js.Runtime) *RequireModule { + rrt := &RequireModule{ + r: r, + runtime: runtime, + modules: make(map[string]*js.Object), + nodeModules: make(map[string]*js.Object), + } + + runtime.Set("require", rrt.require) + return rrt +} + +func (r *Registry) RegisterNativeModule(name string, loader ModuleLoader) { + r.Lock() + defer r.Unlock() + + if r.native == nil { + r.native = make(map[string]ModuleLoader) + } + name = filepathClean(name) + r.native[name] = loader +} + +// DefaultSourceLoader is used if none was set (see WithLoader()). It simply loads files from the host's filesystem. +func DefaultSourceLoader(filename string) ([]byte, error) { + f, err := os.Open(filename) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + err = ModuleFileDoesNotExistError + } else if runtime.GOOS == "windows" { + if errors.Is(err, syscall.Errno(0x7b)) { // ERROR_INVALID_NAME, The filename, directory name, or volume label syntax is incorrect. + err = ModuleFileDoesNotExistError + } + } + return nil, err + } + + defer f.Close() + // On some systems (e.g. plan9 and FreeBSD) it is possible to use the standard read() call on directories + // which means we cannot rely on read() returning an error, we have to do stat() instead. + if fi, err := f.Stat(); err == nil { + if fi.IsDir() { + return nil, ModuleFileDoesNotExistError + } + } else { + return nil, err + } + return io.ReadAll(f) +} + +// DefaultPathResolver is used if none was set (see WithPathResolver). It converts the path using filepath.FromSlash(), +// then joins it with base and resolves symlinks on the resulting path. +// Note, it does not make the path absolute, so to match nodejs behaviour, the initial script name should be set +// to an absolute path. +// The implementation is somewhat suboptimal because it runs filepath.EvalSymlinks() on the joint path, not using the +// fact that the base path is already resolved. This is because there is no way to resolve symlinks only in a portion +// of a path without re-implementing a significant part of filepath.FromSlash(). +func DefaultPathResolver(base, path string) string { + p := filepath.Join(base, filepath.FromSlash(path)) + if resolved, err := filepath.EvalSymlinks(p); err == nil { + p = resolved + } + return p +} + +func (r *Registry) getSource(p string) ([]byte, error) { + srcLoader := r.srcLoader + if srcLoader == nil { + srcLoader = DefaultSourceLoader + } + return srcLoader(p) +} + +func (r *Registry) getCompiledSource(p string) (*js.Program, error) { + r.Lock() + defer r.Unlock() + + prg := r.compiled[p] + if prg == nil { + buf, err := r.getSource(p) + if err != nil { + return nil, err + } + s := string(buf) + + if filepath.Ext(p) == ".json" { + s = "module.exports = JSON.parse('" + template.JSEscapeString(s) + "')" + } + + source := "(function(exports,require,module,__filename,__dirname){" + s + "\n})" + parsed, err := js.Parse(p, source, parser.WithSourceMapLoader(r.srcLoader)) + if err != nil { + return nil, err + } + prg, err = js.CompileAST(parsed, false) + if err == nil { + if r.compiled == nil { + r.compiled = make(map[string]*js.Program) + } + r.compiled[p] = prg + } + return prg, err + } + return prg, nil +} + +func (r *RequireModule) require(call js.FunctionCall) js.Value { + ret, err := r.Require(call.Argument(0).String()) + if err != nil { + if _, ok := err.(*js.Exception); !ok { + panic(r.runtime.NewGoError(err)) + } + panic(err) + } + return ret +} + +func filepathClean(p string) string { + return path.Clean(p) +} + +// Require can be used to import modules from Go source (similar to JS require() function). +func (r *RequireModule) Require(p string) (ret js.Value, err error) { + module, err := r.resolve(p) + if err != nil { + return + } + ret = module.Get("exports") + return +} + +func Require(runtime *js.Runtime, name string) js.Value { + if r, ok := js.AssertFunction(runtime.Get("require")); ok { + mod, err := r(js.Undefined(), runtime.ToValue(name)) + if err != nil { + panic(err) + } + return mod + } + panic(runtime.NewTypeError("Please enable require for this runtime using new(require.Registry).Enable(runtime)")) +} + +// RegisterNativeModule registers a module that isn't loaded through a SourceLoader, but rather through +// a provided ModuleLoader. Typically, this will be a module implemented in Go (although theoretically +// it can be anything, depending on the ModuleLoader implementation). +// Such modules take precedence over modules loaded through a SourceLoader, i.e. if a module name resolves as +// native, the native module is loaded, and the SourceLoader is not consulted. +// The binding is global and affects all instances of Registry. +// It should be called from a package init() function as it may not be used concurrently with require() calls. +// For registry-specific bindings see Registry.RegisterNativeModule. +func RegisterNativeModule(name string, loader ModuleLoader) { + if native == nil { + native = make(map[string]ModuleLoader) + } + name = filepathClean(name) + native[name] = loader +} + +// RegisterCoreModule registers a nodejs core module. If the name does not start with "node:", the module +// will also be loadable as "node:". Hence, for "builtin" modules (such as buffer, console, etc.) +// the name should not include the "node:" prefix, but for prefix-only core modules (such as "node:test") +// it should include the prefix. +func RegisterCoreModule(name string, loader ModuleLoader) { + if builtin == nil { + builtin = make(map[string]ModuleLoader) + } + name = filepathClean(name) + builtin[name] = loader +} diff --git a/require/module_test.go b/require/module_test.go new file mode 100644 index 0000000..20c9076 --- /dev/null +++ b/require/module_test.go @@ -0,0 +1,587 @@ +package require + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "path" + "path/filepath" + "runtime" + "syscall" + "testing" + + js "github.com/dop251/goja" +) + +func mapFileSystemSourceLoader(files map[string]string) SourceLoader { + return func(p string) ([]byte, error) { + s, ok := files[filepath.ToSlash(p)] + if !ok { + return nil, ModuleFileDoesNotExistError + } + return []byte(s), nil + } +} + +func TestRequireNativeModule(t *testing.T) { + const SCRIPT = ` + var m = require("test/m"); + m.test(); + ` + + vm := js.New() + + registry := new(Registry) + registry.Enable(vm) + + RegisterNativeModule("test/m", func(runtime *js.Runtime, module *js.Object) { + o := module.Get("exports").(*js.Object) + o.Set("test", func(call js.FunctionCall) js.Value { + return runtime.ToValue("passed") + }) + }) + + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(vm.ToValue("passed")) { + t.Fatalf("Unexpected result: %v", v) + } +} + +func TestRegisterCoreModule(t *testing.T) { + vm := js.New() + + registry := new(Registry) + registry.Enable(vm) + + RegisterCoreModule("coremod", func(runtime *js.Runtime, module *js.Object) { + o := module.Get("exports").(*js.Object) + o.Set("test", func(call js.FunctionCall) js.Value { + return runtime.ToValue("passed") + }) + }) + + RegisterCoreModule("coremod1", func(runtime *js.Runtime, module *js.Object) { + o := module.Get("exports").(*js.Object) + o.Set("test", func(call js.FunctionCall) js.Value { + return runtime.ToValue("passed1") + }) + }) + + RegisterCoreModule("node:test1", func(runtime *js.Runtime, module *js.Object) { + o := module.Get("exports").(*js.Object) + o.Set("test", func(call js.FunctionCall) js.Value { + return runtime.ToValue("test1 passed") + }) + }) + + registry.RegisterNativeModule("bob", func(runtime *js.Runtime, module *js.Object) { + + }) + + _, err := vm.RunString(` + const m1 = require("coremod"); + const m2 = require("node:coremod"); + if (m1 !== m2) { + throw new Error("Modules are not equal"); + } + if (m1.test() !== "passed") { + throw new Error("m1.test() has failed"); + } + + const m3 = require("node:coremod1"); + const m4 = require("coremod1"); + if (m3 !== m4) { + throw new Error("Modules are not equal (1)"); + } + if (m3.test() !== "passed1") { + throw new Error("m3.test() has failed"); + } + + try { + require("node:bob"); + } catch (e) { + if (!e.message.includes("No such built-in module")) { + throw e; + } + } + require("bob"); + + try { + require("test1"); + throw new Error("Expected exception"); + } catch (e) { + if (!e.message.includes("Invalid module")) { + throw e; + } + } + + if (require("node:test1").test() !== "test1 passed") { + throw new Error("test1.test() has failed"); + } + `) + + if err != nil { + t.Fatal(err) + } +} + +func TestRequireRegistryNativeModule(t *testing.T) { + const SCRIPT = ` + var log = require("test/log"); + log.print('passed'); + ` + + logWithOutput := func(w io.Writer, prefix string) ModuleLoader { + return func(vm *js.Runtime, module *js.Object) { + o := module.Get("exports").(*js.Object) + o.Set("print", func(call js.FunctionCall) js.Value { + fmt.Fprint(w, prefix, call.Argument(0).String()) + return js.Undefined() + }) + } + } + + vm1 := js.New() + buf1 := &bytes.Buffer{} + + registry1 := new(Registry) + registry1.Enable(vm1) + + registry1.RegisterNativeModule("test/log", logWithOutput(buf1, "vm1 ")) + + vm2 := js.New() + buf2 := &bytes.Buffer{} + + registry2 := new(Registry) + registry2.Enable(vm2) + + registry2.RegisterNativeModule("test/log", logWithOutput(buf2, "vm2 ")) + + _, err := vm1.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + s := buf1.String() + if s != "vm1 passed" { + t.Fatalf("vm1: Unexpected result: %q", s) + } + + _, err = vm2.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + s = buf2.String() + if s != "vm2 passed" { + t.Fatalf("vm2: Unexpected result: %q", s) + } +} + +func TestRequire(t *testing.T) { + absPath, err := filepath.Abs("./testdata/m.js") + if err != nil { + t.Fatal(err) + } + + isWindows := runtime.GOOS == "windows" + + tests := []struct { + path string + ok bool + }{ + { + "./testdata/m.js", + true, + }, + { + "../require/testdata/m.js", + true, + }, + { + absPath, + true, + }, + { + `.\testdata\m.js`, + isWindows, + }, + { + `..\require\testdata\m.js`, + isWindows, + }, + } + + const SCRIPT = ` + var m = require(testPath); + m.test(); + ` + + for _, test := range tests { + t.Run(test.path, func(t *testing.T) { + vm := js.New() + vm.Set("testPath", test.path) + + registry := new(Registry) + registry.Enable(vm) + + v, err := vm.RunString(SCRIPT) + + ok := err == nil + + if ok != test.ok { + t.Fatalf("Expected ok to be %v, got %v (%v)", test.ok, ok, err) + } + + if !ok { + return + } + + if !v.StrictEquals(vm.ToValue("passed")) { + t.Fatalf("Unexpected result: %v", v) + } + }) + } +} + +func TestSourceLoader(t *testing.T) { + const SCRIPT = ` + var m = require("m.js"); + m.test(); + ` + + const MODULE = ` + function test() { + return "passed1"; + } + + exports.test = test; + ` + + vm := js.New() + + registry := NewRegistry(WithGlobalFolders("."), WithLoader(func(name string) ([]byte, error) { + if name == "m.js" { + return []byte(MODULE), nil + } + return nil, errors.New("Module does not exist") + })) + registry.Enable(vm) + + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(vm.ToValue("passed1")) { + t.Fatalf("Unexpected result: %v", v) + } +} + +func TestStrictModule(t *testing.T) { + const SCRIPT = ` + var m = require("m.js"); + m.test(); + ` + + const MODULE = ` + "use strict"; + + function test() { + var a = "passed1"; + eval("var a = 'not passed'"); + return a; + } + + exports.test = test; + ` + + vm := js.New() + + registry := NewRegistry(WithGlobalFolders("."), WithLoader(func(name string) ([]byte, error) { + if name == "m.js" { + return []byte(MODULE), nil + } + return nil, errors.New("Module does not exist") + })) + registry.Enable(vm) + + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(vm.ToValue("passed1")) { + t.Fatalf("Unexpected result: %v", v) + } +} + +func TestResolve(t *testing.T) { + testRequire := func(src, fpath string, globalFolders []string, fs map[string]string) (*js.Runtime, js.Value, error) { + vm := js.New() + r := NewRegistry(WithGlobalFolders(globalFolders...), WithLoader(mapFileSystemSourceLoader(fs))) + r.Enable(vm) + t.Logf("Require(%s)", fpath) + ret, err := vm.RunScript(path.Join(src, "test.js"), fmt.Sprintf("require('%s')", fpath)) + if err != nil { + return nil, nil, err + } + return vm, ret, nil + } + + globalFolders := []string{ + "/usr/lib/node_modules", + "/home/src/.node_modules", + } + + fs := map[string]string{ + "/home/src/app/app.js": `exports.name = "app"`, + "/home/src/app2/app2.json": `{"name": "app2"}`, + "/home/src/app3/index.js": `exports.name = "app3"`, + "/home/src/app4/index.json": `{"name": "app4"}`, + "/home/src/app5/package.json": `{"main": "app5.js"}`, + "/home/src/app5/app5.js": `exports.name = "app5"`, + "/home/src/app6/package.json": `{"main": "."}`, + "/home/src/app6/index.js": `exports.name = "app6"`, + "/home/src/app7/package.json": `{"main": "./a/b/c/file.js"}`, + "/home/src/app7/a/b/c/file.js": `exports.name = "app7"`, + "/usr/lib/node_modules/app8": `exports.name = "app8"`, + "/home/src/app9/app9.js": `exports.name = require('./a/file.js').name`, + "/home/src/app9/a/file.js": `exports.name = require('./b/file.js').name`, + "/home/src/app9/a/b/file.js": `exports.name = require('./c/file.js').name`, + "/home/src/app9/a/b/c/file.js": `exports.name = "app9"`, + "/home/src/.node_modules/app10": `exports.name = "app10"`, + "/home/src/app11/app11.js": `exports.name = require('d/file.js').name`, + "/home/src/app11/a/b/c/app11.js": `exports.name = require('d/file.js').name`, + "/home/src/app11/node_modules/d/file.js": `exports.name = "app11"`, + "/app12.js": `exports.name = require('a/file.js').name`, + "/node_modules/a/file.js": `exports.name = "app12"`, + "/app13/app13.js": `exports.name = require('b/file.js').name`, + "/node_modules/b/file.js": `exports.name = "app13"`, + "node_modules/app14/index.js": `exports.name = "app14"`, + "../node_modules/app15/index.js": `exports.name = "app15"`, + } + + for i, tc := range []struct { + src string + path string + ok bool + field string + value string + }{ + {"/home/src", "./app/app", true, "name", "app"}, + {"/home/src", "./app/app.js", true, "name", "app"}, + {"/home/src", "./app/bad.js", false, "", ""}, + {"/home/src", "./app2/app2", true, "name", "app2"}, + {"/home/src", "./app2/app2.json", true, "name", "app2"}, + {"/home/src", "./app/bad.json", false, "", ""}, + {"/home/src", "./app3", true, "name", "app3"}, + {"/home/src", "./appbad", false, "", ""}, + {"/home/src", "./app4", true, "name", "app4"}, + {"/home/src", "./appbad", false, "", ""}, + {"/home/src", "./app5", true, "name", "app5"}, + {"/home/src", "./app6", true, "name", "app6"}, + {"/home/src", "./app7", true, "name", "app7"}, + {"/home/src", "app8", true, "name", "app8"}, + {"/home/src", "./app9/app9", true, "name", "app9"}, + {"/home/src", "app10", true, "name", "app10"}, + {"/home/src", "./app11/app11.js", true, "name", "app11"}, + {"/home/src", "./app11/a/b/c/app11.js", true, "name", "app11"}, + {"/", "./app12", true, "name", "app12"}, + {"/", "./app13/app13", true, "name", "app13"}, + {".", "app14", true, "name", "app14"}, + {"..", "nonexistent", false, "", ""}, + } { + vm, mod, err := testRequire(tc.src, tc.path, globalFolders, fs) + if err != nil { + if tc.ok { + t.Errorf("%d: require() failed: %v", i, err) + } + continue + } else { + if !tc.ok { + t.Errorf("%d: expected to fail, but did not", i) + continue + } + } + f := mod.ToObject(vm).Get(tc.field) + if f == nil { + t.Errorf("%v: field %q not found", i, tc.field) + continue + } + value := f.String() + if value != tc.value { + t.Errorf("%v: got %q expected %q", i, value, tc.value) + } + } +} + +func TestRequireCycle(t *testing.T) { + vm := js.New() + r := NewRegistry(WithLoader(mapFileSystemSourceLoader(map[string]string{ + "a.js": `var b = require('./b.js'); exports.done = true;`, + "b.js": `var a = require('./a.js'); exports.done = true;`, + }))) + r.Enable(vm) + res, err := vm.RunString(` + var a = require('./a.js'); + var b = require('./b.js'); + a.done && b.done; + `) + if err != nil { + t.Fatal(err) + } + if v := res.Export(); v != true { + t.Fatalf("Unexpected result: %v", v) + } +} + +func TestErrorPropagation(t *testing.T) { + vm := js.New() + r := NewRegistry(WithLoader(mapFileSystemSourceLoader(map[string]string{ + "m.js": `throw 'test passed';`, + }))) + rr := r.Enable(vm) + _, err := rr.Require("./m") + if err == nil { + t.Fatal("Expected an error") + } + if ex, ok := err.(*js.Exception); ok { + if !ex.Value().StrictEquals(vm.ToValue("test passed")) { + t.Fatalf("Unexpected Exception: %v", ex) + } + } else { + t.Fatal(err) + } +} + +func TestSourceMapLoader(t *testing.T) { + vm := js.New() + r := NewRegistry(WithLoader(func(p string) ([]byte, error) { + switch filepath.ToSlash(p) { + case "dir/m.js": + return []byte(`throw 'test passed'; +//# sourceMappingURL=m.js.map`), nil + case "dir/m.js.map": + return []byte(`{"version":3,"file":"m.js","sourceRoot":"","sources":["m.ts"],"names":[],"mappings":";AAAA"} +`), nil + } + return nil, ModuleFileDoesNotExistError + })) + + rr := r.Enable(vm) + _, err := rr.Require("./dir/m") + if err == nil { + t.Fatal("Expected an error") + } + if ex, ok := err.(*js.Exception); ok { + if !ex.Value().StrictEquals(vm.ToValue("test passed")) { + t.Fatalf("Unexpected Exception: %v", ex) + } + } else { + t.Fatal(err) + } +} + +func testsetup() (string, func(), error) { + name, err := os.MkdirTemp("", "goja-nodejs-require-test") + if err != nil { + return "", nil, err + } + return name, func() { + os.RemoveAll(name) + }, nil +} + +func TestDefaultModuleLoader(t *testing.T) { + workdir, teardown, err := testsetup() + if err != nil { + t.Fatal(err) + } + defer teardown() + + err = os.Chdir(workdir) + if err != nil { + t.Fatal(err) + } + err = os.Mkdir("module", 0755) + if err != nil { + t.Fatal(err) + } + err = os.WriteFile("module/index.js", []byte(`throw 'test passed';`), 0644) + if err != nil { + t.Fatal(err) + } + vm := js.New() + r := NewRegistry() + rr := r.Enable(vm) + _, err = rr.Require("./module") + if err == nil { + t.Fatal("Expected an error") + } + if ex, ok := err.(*js.Exception); ok { + if !ex.Value().StrictEquals(vm.ToValue("test passed")) { + t.Fatalf("Unexpected Exception: %v", ex) + } + } else { + t.Fatal(err) + } +} + +func TestDefaultPathResolver(t *testing.T) { + workdir, teardown, err := testsetup() + if err != nil { + t.Fatal(err) + } + defer teardown() + + err = os.Chdir(workdir) + if err != nil { + t.Fatal(err) + } + err = os.Mkdir("node_modules", 0755) + if err != nil { + t.Fatal(err) + } + err = os.Mkdir("node_modules/a", 0755) + if err != nil { + t.Fatal(err) + } + err = os.Mkdir("node_modules/a/node_modules", 0755) + if err != nil { + t.Fatal(err) + } + err = os.Mkdir("node_modules/b", 0755) + if err != nil { + t.Fatal(err) + } + err = os.Symlink("../../b", "node_modules/a/node_modules/b") + if err != nil { + if runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(1314)) { // ERROR_PRIVILEGE_NOT_HELD + t.Skip("Creating symlinks on Windows requires admin privileges") + } + t.Fatal(err) + } + + err = os.WriteFile("node_modules/b/index.js", []byte(``), 0644) + if err != nil { + t.Fatal(err) + } + err = os.WriteFile("node_modules/a/index.js", []byte(`require('b')`), 0644) + if err != nil { + t.Fatal(err) + } + vm := js.New() + r := NewRegistry() + rr := r.Enable(vm) + _, err = rr.Require("a") + if err != nil { + t.Fatal(err) + } +} diff --git a/require/resolve.go b/require/resolve.go new file mode 100644 index 0000000..064c89a --- /dev/null +++ b/require/resolve.go @@ -0,0 +1,275 @@ +package require + +import ( + "encoding/json" + "errors" + "path/filepath" + "runtime" + "strings" + + js "github.com/dop251/goja" +) + +const NodePrefix = "node:" + +func (r *RequireModule) resolvePath(base, name string) string { + if r.r.pathResolver != nil { + return r.r.pathResolver(base, name) + } + return DefaultPathResolver(base, name) +} + +// NodeJS module search algorithm described by +// https://nodejs.org/api/modules.html#modules_all_together +func (r *RequireModule) resolve(modpath string) (module *js.Object, err error) { + var start string + err = nil + if !filepath.IsAbs(modpath) { + start = r.getCurrentModulePath() + } + + p := r.resolvePath(start, modpath) + if isFileOrDirectoryPath(modpath) { + if module = r.modules[p]; module != nil { + return + } + module, err = r.loadAsFileOrDirectory(p) + if err == nil && module != nil { + r.modules[p] = module + } + } else { + module, err = r.loadNative(modpath) + if err == nil { + return + } else { + if err == InvalidModuleError { + err = nil + } else { + return + } + } + if module = r.nodeModules[p]; module != nil { + return + } + module, err = r.loadNodeModules(modpath, start) + if err == nil && module != nil { + r.nodeModules[p] = module + } + } + + if module == nil && err == nil { + err = InvalidModuleError + } + return +} + +func (r *RequireModule) loadNative(path string) (*js.Object, error) { + module := r.modules[path] + if module != nil { + return module, nil + } + + var ldr ModuleLoader + if ldr = r.r.native[path]; ldr == nil { + ldr = native[path] + } + + var isBuiltIn, withPrefix bool + if ldr == nil { + ldr = builtin[path] + if ldr == nil && strings.HasPrefix(path, NodePrefix) { + ldr = builtin[path[len(NodePrefix):]] + if ldr == nil { + return nil, NoSuchBuiltInModuleError + } + withPrefix = true + } + isBuiltIn = true + } + + if ldr != nil { + module = r.createModuleObject() + r.modules[path] = module + if isBuiltIn { + if withPrefix { + r.modules[path[len(NodePrefix):]] = module + } else { + if !strings.HasPrefix(path, NodePrefix) { + r.modules[NodePrefix+path] = module + } + } + } + ldr(r.runtime, module) + return module, nil + } + + return nil, InvalidModuleError +} + +func (r *RequireModule) loadAsFileOrDirectory(path string) (module *js.Object, err error) { + if module, err = r.loadAsFile(path); module != nil || err != nil { + return + } + + return r.loadAsDirectory(path) +} + +func (r *RequireModule) loadAsFile(path string) (module *js.Object, err error) { + if module, err = r.loadModule(path); module != nil || err != nil { + return + } + + p := path + ".js" + if module, err = r.loadModule(p); module != nil || err != nil { + return + } + + p = path + ".json" + return r.loadModule(p) +} + +func (r *RequireModule) loadIndex(modpath string) (module *js.Object, err error) { + p := r.resolvePath(modpath, "index.js") + if module, err = r.loadModule(p); module != nil || err != nil { + return + } + + p = r.resolvePath(modpath, "index.json") + return r.loadModule(p) +} + +func (r *RequireModule) loadAsDirectory(modpath string) (module *js.Object, err error) { + p := r.resolvePath(modpath, "package.json") + buf, err := r.r.getSource(p) + if err != nil { + return r.loadIndex(modpath) + } + var pkg struct { + Main string + } + err = json.Unmarshal(buf, &pkg) + if err != nil || len(pkg.Main) == 0 { + return r.loadIndex(modpath) + } + + m := r.resolvePath(modpath, pkg.Main) + if module, err = r.loadAsFile(m); module != nil || err != nil { + return + } + + return r.loadIndex(m) +} + +func (r *RequireModule) loadNodeModule(modpath, start string) (*js.Object, error) { + return r.loadAsFileOrDirectory(r.resolvePath(start, modpath)) +} + +func (r *RequireModule) loadNodeModules(modpath, start string) (module *js.Object, err error) { + for _, dir := range r.r.globalFolders { + if module, err = r.loadNodeModule(modpath, dir); module != nil || err != nil { + return + } + } + for { + var p string + if filepath.Base(start) != "node_modules" { + p = filepath.Join(start, "node_modules") + } else { + p = start + } + if module, err = r.loadNodeModule(modpath, p); module != nil || err != nil { + return + } + if start == ".." { // Dir('..') is '.' + break + } + parent := filepath.Dir(start) + if parent == start { + break + } + start = parent + } + + return +} + +func (r *RequireModule) getCurrentModulePath() string { + var buf [2]js.StackFrame + frames := r.runtime.CaptureCallStack(2, buf[:0]) + if len(frames) < 2 { + return "." + } + return filepath.Dir(frames[1].SrcName()) +} + +func (r *RequireModule) createModuleObject() *js.Object { + module := r.runtime.NewObject() + module.Set("exports", r.runtime.NewObject()) + return module +} + +func (r *RequireModule) loadModule(path string) (*js.Object, error) { + module := r.modules[path] + if module == nil { + module = r.createModuleObject() + r.modules[path] = module + err := r.loadModuleFile(path, module) + if err != nil { + module = nil + delete(r.modules, path) + if errors.Is(err, ModuleFileDoesNotExistError) { + err = nil + } + } + return module, err + } + return module, nil +} + +func (r *RequireModule) loadModuleFile(path string, jsModule *js.Object) error { + + prg, err := r.r.getCompiledSource(path) + + if err != nil { + return err + } + + f, err := r.runtime.RunProgram(prg) + if err != nil { + return err + } + + if call, ok := js.AssertFunction(f); ok { + jsExports := jsModule.Get("exports") + jsRequire := r.runtime.Get("require") + + // Run the module source, with "jsExports" as "this", + // "jsExports" as the "exports" variable, "jsRequire" + // as the "require" variable and "jsModule" as the + // "module" variable (Nodejs capable). + _, err = call(jsExports, jsExports, jsRequire, jsModule, r.runtime.ToValue(path), r.runtime.ToValue(filepath.Dir(path))) + if err != nil { + return err + } + } else { + return InvalidModuleError + } + + return nil +} + +func isFileOrDirectoryPath(path string) bool { + result := path == "." || path == ".." || + strings.HasPrefix(path, "/") || + strings.HasPrefix(path, "./") || + strings.HasPrefix(path, "../") + + if runtime.GOOS == "windows" { + result = result || + strings.HasPrefix(path, `.\`) || + strings.HasPrefix(path, `..\`) || + filepath.IsAbs(path) + } + + return result +} diff --git a/require/testdata/m.js b/require/testdata/m.js new file mode 100644 index 0000000..97d9995 --- /dev/null +++ b/require/testdata/m.js @@ -0,0 +1,7 @@ +function test() { + return "passed"; +} + +module.exports = { + test: test +} diff --git a/staticcheck.conf b/staticcheck.conf new file mode 100644 index 0000000..2441b9d --- /dev/null +++ b/staticcheck.conf @@ -0,0 +1 @@ +checks = ["all", "-ST1000", "-ST1003", "-ST1005", "-ST1006", "-ST1012", "-ST1021", "-ST1020", "-ST1008"] diff --git a/url/escape.go b/url/escape.go new file mode 100644 index 0000000..3d288c2 --- /dev/null +++ b/url/escape.go @@ -0,0 +1,134 @@ +package url + +import "strings" + +var tblEscapeURLQuery = [128]byte{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, +} + +var tblEscapeURLQueryParam = [128]byte{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, +} + +// The code below is mostly borrowed from the standard Go url package + +const upperhex = "0123456789ABCDEF" + +func ishex(c byte) bool { + switch { + case '0' <= c && c <= '9': + return true + case 'a' <= c && c <= 'f': + return true + case 'A' <= c && c <= 'F': + return true + } + return false +} + +func unhex(c byte) byte { + switch { + case '0' <= c && c <= '9': + return c - '0' + case 'a' <= c && c <= 'f': + return c - 'a' + 10 + case 'A' <= c && c <= 'F': + return c - 'A' + 10 + } + return 0 +} + +func escape(s string, table *[128]byte, spaceToPlus bool) string { + spaceCount, hexCount := 0, 0 + for i := 0; i < len(s); i++ { + c := s[i] + if c > 127 || table[c] == 0 { + if c == ' ' && spaceToPlus { + spaceCount++ + } else { + hexCount++ + } + } + } + + if spaceCount == 0 && hexCount == 0 { + return s + } + + var sb strings.Builder + hexBuf := [3]byte{'%', 0, 0} + + sb.Grow(len(s) + 2*hexCount) + + for i := 0; i < len(s); i++ { + switch c := s[i]; { + case c == ' ' && spaceToPlus: + sb.WriteByte('+') + case c > 127 || table[c] == 0: + hexBuf[1] = upperhex[c>>4] + hexBuf[2] = upperhex[c&15] + sb.Write(hexBuf[:]) + default: + sb.WriteByte(c) + } + } + return sb.String() +} + +func unescapeSearchParam(s string) string { + n := 0 + hasPlus := false + for i := 0; i < len(s); { + switch s[i] { + case '%': + if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) { + i++ + continue + } + n++ + i += 3 + case '+': + hasPlus = true + i++ + default: + i++ + } + } + + if n == 0 && !hasPlus { + return s + } + + var t strings.Builder + t.Grow(len(s) - 2*n) + for i := 0; i < len(s); i++ { + switch s[i] { + case '%': + if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) { + t.WriteByte('%') + } else { + t.WriteByte(unhex(s[i+1])<<4 | unhex(s[i+2])) + i += 2 + } + case '+': + t.WriteByte(' ') + default: + t.WriteByte(s[i]) + } + } + return t.String() +} diff --git a/url/module.go b/url/module.go new file mode 100644 index 0000000..5f84533 --- /dev/null +++ b/url/module.go @@ -0,0 +1,36 @@ +package url + +import ( + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/require" +) + +const ModuleName = "url" + +type urlModule struct { + r *goja.Runtime + + URLSearchParamsPrototype *goja.Object + URLSearchParamsIteratorPrototype *goja.Object +} + +func Require(runtime *goja.Runtime, module *goja.Object) { + exports := module.Get("exports").(*goja.Object) + m := &urlModule{ + r: runtime, + } + exports.Set("URL", m.createURLConstructor()) + exports.Set("URLSearchParams", m.createURLSearchParamsConstructor()) + exports.Set("domainToASCII", m.domainToASCII) + exports.Set("domainToUnicode", m.domainToUnicode) +} + +func Enable(runtime *goja.Runtime) { + m := require.Require(runtime, ModuleName).ToObject(runtime) + runtime.Set("URL", m.Get("URL")) + runtime.Set("URLSearchParams", m.Get("URLSearchParams")) +} + +func init() { + require.RegisterCoreModule(ModuleName, Require) +} diff --git a/url/nodeurl.go b/url/nodeurl.go new file mode 100644 index 0000000..538cccf --- /dev/null +++ b/url/nodeurl.go @@ -0,0 +1,148 @@ +package url + +import ( + "net/url" + "strings" +) + +type searchParam struct { + name string + value string +} + +func (sp *searchParam) Encode() string { + return sp.string(true) +} + +func escapeSearchParam(s string) string { + return escape(s, &tblEscapeURLQueryParam, true) +} + +func (sp *searchParam) string(encode bool) string { + if encode { + return escapeSearchParam(sp.name) + "=" + escapeSearchParam(sp.value) + } else { + return sp.name + "=" + sp.value + } +} + +type searchParams []searchParam + +func (s searchParams) Len() int { + return len(s) +} + +func (s searchParams) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s searchParams) Less(i, j int) bool { + return strings.Compare(s[i].name, s[j].name) < 0 +} + +func (s searchParams) Encode() string { + var sb strings.Builder + for i, v := range s { + if i > 0 { + sb.WriteByte('&') + } + sb.WriteString(v.Encode()) + } + return sb.String() +} + +func (s searchParams) String() string { + var sb strings.Builder + for i, v := range s { + if i > 0 { + sb.WriteByte('&') + } + sb.WriteString(v.string(false)) + } + return sb.String() +} + +type nodeURL struct { + url *url.URL + searchParams searchParams +} + +type urlSearchParams nodeURL + +// This methods ensures that the url.URL has the proper RawQuery based on the searchParam +// structs. If a change is made to the searchParams we need to keep them in sync. +func (nu *nodeURL) syncSearchParams() { + if nu.rawQueryUpdateNeeded() { + nu.url.RawQuery = nu.searchParams.Encode() + } +} + +func (nu *nodeURL) rawQueryUpdateNeeded() bool { + return len(nu.searchParams) > 0 && nu.url.RawQuery == "" +} + +func (nu *nodeURL) String() string { + return nu.url.String() +} + +func (sp *urlSearchParams) hasName(name string) bool { + for _, v := range sp.searchParams { + if v.name == name { + return true + } + } + return false +} + +func (sp *urlSearchParams) hasValue(name, value string) bool { + for _, v := range sp.searchParams { + if v.name == name && v.value == value { + return true + } + } + return false +} + +func (sp *urlSearchParams) getValues(name string) []string { + vals := make([]string, 0, len(sp.searchParams)) + for _, v := range sp.searchParams { + if v.name == name { + vals = append(vals, v.value) + } + } + + return vals +} + +func (sp *urlSearchParams) getFirstValue(name string) (string, bool) { + for _, v := range sp.searchParams { + if v.name == name { + return v.value, true + } + } + + return "", false +} + +func parseSearchQuery(query string) (ret searchParams) { + if query == "" { + return + } + + query = strings.TrimPrefix(query, "?") + + for _, v := range strings.Split(query, "&") { + if v == "" { + continue + } + pair := strings.SplitN(v, "=", 2) + l := len(pair) + if l == 1 { + ret = append(ret, searchParam{name: unescapeSearchParam(pair[0]), value: ""}) + } else if l == 2 { + ret = append(ret, searchParam{name: unescapeSearchParam(pair[0]), value: unescapeSearchParam(pair[1])}) + } + } + + return +} diff --git a/url/testdata/url_search_params.js b/url/testdata/url_search_params.js new file mode 100644 index 0000000..93ca7ef --- /dev/null +++ b/url/testdata/url_search_params.js @@ -0,0 +1,387 @@ +"use strict"; + +const assert = require("../../assert.js"); + +let params; + +function testCtor(value, expected) { + assert.sameValue(new URLSearchParams(value).toString(), expected); +} + +testCtor("user=abc&query=xyz", "user=abc&query=xyz"); +testCtor("?user=abc&query=xyz", "user=abc&query=xyz"); + +testCtor( + { + num: 1, + user: "abc", + query: ["first", "second"], + obj: { prop: "value" }, + b: true, + }, + "num=1&user=abc&query=first%2Csecond&obj=%5Bobject+Object%5D&b=true" +); + +const map = new Map(); +map.set("user", "abc"); +map.set("query", "xyz"); +testCtor(map, "user=abc&query=xyz"); + +testCtor( + [ + ["user", "abc"], + ["query", "first"], + ["query", "second"], + ], + "user=abc&query=first&query=second" +); + +// Each key-value pair must have exactly two elements +assert.throwsNodeError(() => new URLSearchParams([["single_value"]]), TypeError, "ERR_INVALID_TUPLE"); +assert.throwsNodeError(() => new URLSearchParams([["too", "many", "values"]]), TypeError, "ERR_INVALID_TUPLE"); + +params = new URLSearchParams("a=b&cc=d"); +params.forEach((value, name, searchParams) => { + if (name === "a") { + assert.sameValue(value, "b"); + } + if (name === "cc") { + assert.sameValue(value, "d"); + } + assert.sameValue(searchParams, params); +}); + +params.forEach((value, name, searchParams) => { + if (name === "a") { + assert.sameValue(value, "b"); + searchParams.set("cc", "d1"); + } + if (name === "cc") { + assert.sameValue(value, "d1"); + } + assert.sameValue(searchParams, params); +}); + +assert.throwsNodeError(() => params.forEach(123), TypeError, "ERR_INVALID_ARG_TYPE"); + +assert.throwsNodeError(() => params.forEach.call(1, 2), TypeError, "ERR_INVALID_THIS"); + +params = new URLSearchParams("a=1=2&b=3"); +assert.sameValue(params.size, 2); +assert.sameValue(params.get("a"), "1=2"); +assert.sameValue(params.get("b"), "3"); + +params = new URLSearchParams("&"); +assert.sameValue(params.size, 0); + +params = new URLSearchParams("& "); +assert.sameValue(params.size, 1); +assert.sameValue(params.get(" "), ""); + +params = new URLSearchParams(" &"); +assert.sameValue(params.size, 1); +assert.sameValue(params.get(" "), ""); + +params = new URLSearchParams("="); +assert.sameValue(params.size, 1); +assert.sameValue(params.get(""), ""); + +params = new URLSearchParams("&=2"); +assert.sameValue(params.size, 1); +assert.sameValue(params.get(""), "2"); + +params = new URLSearchParams("?user=abc"); +assert.throwsNodeError(() => params.append(), TypeError, "ERR_MISSING_ARGS"); +params.append("query", "first"); +assert.sameValue(params.toString(), "user=abc&query=first"); + +params = new URLSearchParams("first=one&second=two&third=three"); +assert.throwsNodeError(() => params.delete(), TypeError, "ERR_MISSING_ARGS"); +params.delete("second", "fake-value"); +assert.sameValue(params.toString(), "first=one&second=two&third=three"); +params.delete("third", "three"); +assert.sameValue(params.toString(), "first=one&second=two"); +params.delete("second"); +assert.sameValue(params.toString(), "first=one"); + +params = new URLSearchParams("user=abc&query=xyz"); +assert.throwsNodeError(() => params.get(), TypeError, "ERR_MISSING_ARGS"); +assert.sameValue(params.get("user"), "abc"); +assert.sameValue(params.get("non-existant"), null); + +params = new URLSearchParams("query=first&query=second"); +assert.throwsNodeError(() => params.getAll(), TypeError, "ERR_MISSING_ARGS"); +const all = params.getAll("query"); +assert.sameValue(all.includes("first"), true); +assert.sameValue(all.includes("second"), true); +assert.sameValue(all.length, 2); +const getAllUndefined = params.getAll(undefined); +assert.sameValue(getAllUndefined.length, 0); +const getAllNonExistant = params.getAll("does_not_exists"); +assert.sameValue(getAllNonExistant.length, 0); + +params = new URLSearchParams("user=abc&query=xyz"); +assert.throwsNodeError(() => params.has(), TypeError, "ERR_MISSING_ARGS"); +assert.sameValue(params.has(undefined), false); +assert.sameValue(params.has("user"), true); +assert.sameValue(params.has("user", "abc"), true); +assert.sameValue(params.has("user", "abc", "extra-param"), true); +assert.sameValue(params.has("user", "efg"), false); +assert.sameValue(params.has("user", undefined), true); + +params = new URLSearchParams(); +params.append("foo", "bar"); +params.append("foo", "baz"); +params.append("abc", "def"); +assert.sameValue(params.toString(), "foo=bar&foo=baz&abc=def"); +params.set("foo", "def"); +params.set("xyz", "opq"); +assert.sameValue(params.toString(), "foo=def&abc=def&xyz=opq"); + +params = new URLSearchParams("query=first&query=second&user=abc&double=first,second"); +const URLSearchIteratorPrototype = params.entries().__proto__; +assert.sameValue(typeof URLSearchIteratorPrototype, "object"); +assert.sameValue(URLSearchIteratorPrototype.__proto__, [][Symbol.iterator]().__proto__.__proto__); + +assert.sameValue(params[Symbol.iterator], params.entries); + +{ + const entries = params.entries(); + assert.sameValue(entries.toString(), "[object URLSearchParams Iterator]"); + assert.sameValue(entries.__proto__, URLSearchIteratorPrototype); + + let item = entries.next(); + assert.sameValue(item.value.toString(), ["query", "first"].toString()); + assert.sameValue(item.done, false); + + item = entries.next(); + assert.sameValue(item.value.toString(), ["query", "second"].toString()); + assert.sameValue(item.done, false); + + item = entries.next(); + assert.sameValue(item.value.toString(), ["user", "abc"].toString()); + assert.sameValue(item.done, false); + + item = entries.next(); + assert.sameValue(item.value.toString(), ["double", "first,second"].toString()); + assert.sameValue(item.done, false); + + item = entries.next(); + assert.sameValue(item.value, undefined); + assert.sameValue(item.done, true); +} + +params = new URLSearchParams("query=first&query=second&user=abc"); +{ + const keys = params.keys(); + assert.sameValue(keys.__proto__, URLSearchIteratorPrototype); + + let item = keys.next(); + assert.sameValue(item.value, "query"); + assert.sameValue(item.done, false); + + item = keys.next(); + assert.sameValue(item.value, "query"); + assert.sameValue(item.done, false); + + item = keys.next(); + assert.sameValue(item.value, "user"); + assert.sameValue(item.done, false); + + item = keys.next(); + assert.sameValue(item.value, undefined); + assert.sameValue(item.done, true); + + assert.sameValue(Array.from(params.keys()).length, 3); +} + +params = new URLSearchParams("query=first&query=second&user=abc"); +{ + const values = params.values(); + assert.sameValue(values.__proto__, URLSearchIteratorPrototype); + + let item = values.next(); + assert.sameValue(item.value, "first"); + assert.sameValue(item.done, false); + + item = values.next(); + assert.sameValue(item.value, "second"); + assert.sameValue(item.done, false); + + item = values.next(); + assert.sameValue(item.value, "abc"); + assert.sameValue(item.done, false); + + item = values.next(); + assert.sameValue(item.value, undefined); + assert.sameValue(item.done, true); +} + + +params = new URLSearchParams("query[]=abc&type=search&query[]=123"); +params.sort(); +assert.sameValue(params.toString(), "query%5B%5D=abc&query%5B%5D=123&type=search"); + +params = new URLSearchParams("query=first&query=second&user=abc"); +assert.sameValue(params.size, 3); + +params = new URLSearchParams("%"); +assert.sameValue(params.has("%"), true); +assert.sameValue(params.toString(), "%25="); + +{ + const params = new URLSearchParams(""); + assert.sameValue(params.size, 0); + assert.sameValue(params.toString(), ""); + assert.sameValue(params.get(undefined), null); + params.set(undefined, true); + assert.sameValue(params.has(undefined), true); + assert.sameValue(params.has("undefined"), true); + assert.sameValue(params.get("undefined"), "true"); + assert.sameValue(params.get(undefined), "true"); + assert.sameValue(params.getAll(undefined).toString(), ["true"].toString()); + params.delete(undefined); + assert.sameValue(params.has(undefined), false); + assert.sameValue(params.has("undefined"), false); + + assert.sameValue(params.has(null), false); + params.set(null, "nullval"); + assert.sameValue(params.has(null), true); + assert.sameValue(params.has("null"), true); + assert.sameValue(params.get(null), "nullval"); + assert.sameValue(params.get("null"), "nullval"); + params.delete(null); + assert.sameValue(params.has(null), false); + assert.sameValue(params.has("null"), false); +} + +function* functionGeneratorExample() { + yield ["user", "abc"]; + yield ["query", "first"]; + yield ["query", "second"]; +} + +params = new URLSearchParams(functionGeneratorExample()); +assert.sameValue(params.toString(), "user=abc&query=first&query=second"); + +assert.sameValue(params.__proto__.constructor, URLSearchParams); +assert.sameValue(params instanceof URLSearchParams, true); + +{ + const params = new URLSearchParams("1=2&1=3"); + assert.sameValue(params.get(1), "2"); + assert.sameValue(params.getAll(1).toString(), ["2", "3"].toString()); + assert.sameValue(params.getAll("x").toString(), [].toString()); +} + +// Sync +{ + const url = new URL("https://test.com/"); + const params = url.searchParams; + assert.sameValue(params.size, 0); + url.search = "a=1"; + assert.sameValue(params.size, 1); + assert.sameValue(params.get("a"), "1"); +} + +{ + const url = new URL("https://test.com/?a=1"); + const params = url.searchParams; + assert.sameValue(params.size, 1); + url.search = ""; + assert.sameValue(params.size, 0); + url.search = "b=2"; + assert.sameValue(params.size, 1); +} + +{ + const url = new URL("https://test.com/"); + const params = url.searchParams; + params.append("a", "1"); + assert.sameValue(url.toString(), "https://test.com/?a=1"); +} + +{ + const url = new URL("https://test.com/"); + url.searchParams.append("a", "1"); + url.searchParams.append("b", "1"); + assert.sameValue(url.toString(), "https://test.com/?a=1&b=1"); +} + +{ + const url = new URL("https://test.com/"); + url.searchParams.append("a", "1"); + assert.sameValue(url.search, "?a=1"); +} + +{ + const url = new URL("https://test.com/?a=1"); + const params = url.searchParams; + params.append("a", "2"); + assert.sameValue(url.search, "?a=1&a=2"); +} + +{ + const url = new URL("https://test.com/"); + const params = url.searchParams; + params.set("a", "1"); + assert.sameValue(url.search, "?a=1"); +} + +{ + const url = new URL("https://test.com/"); + url.searchParams.set("a", "1"); + url.searchParams.set("b", "1"); + assert.sameValue(url.toString(), "https://test.com/?a=1&b=1"); +} + +{ + const url = new URL("https://test.com/?a=1&b=2"); + const params = url.searchParams; + params.delete("a"); + assert.sameValue(url.search, "?b=2"); +} + +{ + const url = new URL("https://test.com/?b=2&a=1"); + const params = url.searchParams; + params.sort(); + assert.sameValue(url.search, "?a=1&b=2"); +} + +{ + const url = new URL("https://test.com/?a=1"); + const params = url.searchParams; + params.delete("a"); + assert.sameValue(url.search, ""); + + params.set("a", 2); + assert.sameValue(url.search, "?a=2"); +} + +// FAILING: no custom properties on wrapped Go structs +/* +{ + const params = new URLSearchParams(""); + assert.sameValue(Object.isExtensible(params), true); + assert.sameValue(Reflect.defineProperty(params, "customField", {value: 42, configurable: true}), true); + assert.sameValue(params.customField, 42); + const desc = Reflect.getOwnPropertyDescriptor(params, "customField"); + assert.sameValue(desc.value, 42); + assert.sameValue(desc.writable, false); + assert.sameValue(desc.enumerable, false); + assert.sameValue(desc.configurable, true); +} +*/ + +// Escape +{ + const myURL = new URL('https://example.org/abc?fo~o=~ba r%z'); + + assert.sameValue(myURL.search, "?fo~o=~ba%20r%z"); + + // Modify the URL via searchParams... + myURL.searchParams.sort(); + + assert.sameValue(myURL.search, "?fo%7Eo=%7Eba+r%25z"); +} diff --git a/url/testdata/url_test.js b/url/testdata/url_test.js new file mode 100644 index 0000000..5cfd75f --- /dev/null +++ b/url/testdata/url_test.js @@ -0,0 +1,262 @@ +"use strict"; + +const assert = require("../../assert.js"); + +function testURLCtor(str, expected) { + assert.sameValue(new URL(str).toString(), expected); +} + +function testURLCtorBase(ref, base, expected, message) { + assert.sameValue(new URL(ref, base).toString(), expected, message); +} + +testURLCtorBase("https://example.org/", undefined, "https://example.org/"); +testURLCtorBase("/foo", "https://example.org/", "https://example.org/foo"); +testURLCtorBase("http://Example.com/", "https://example.org/", "http://example.com/"); +testURLCtorBase("https://Example.com/", "https://example.org/", "https://example.com/"); +testURLCtorBase("foo://Example.com/", "https://example.org/", "foo://Example.com/"); +testURLCtorBase("foo:Example.com/", "https://example.org/", "foo:Example.com/"); +testURLCtorBase("#hash", "https://example.org/", "https://example.org/#hash"); + +testURLCtor("HTTP://test.com", "http://test.com/"); +testURLCtor("HTTPS://á.com", "https://xn--1ca.com/"); +testURLCtor("HTTPS://á.com:123", "https://xn--1ca.com:123/"); +testURLCtor("https://test.com#asdfá", "https://test.com/#asdf%C3%A1"); +testURLCtor("HTTPS://á.com:123/á", "https://xn--1ca.com:123/%C3%A1"); +testURLCtor("fish://á.com", "fish://%C3%A1.com"); +testURLCtor("https://test.com/?a=1 /2", "https://test.com/?a=1%20/2"); +testURLCtor("https://test.com/á=1?á=1&ü=2#é", "https://test.com/%C3%A1=1?%C3%A1=1&%C3%BC=2#%C3%A9"); + +assert.throws(() => new URL("test"), TypeError); +assert.throws(() => new URL("ssh://EEE:ddd"), TypeError); + +{ + let u = new URL("https://example.org/"); + assert.sameValue(u.__proto__.constructor, URL); + assert.sameValue(u instanceof URL, true); +} + +{ + let u = new URL("https://example.org/"); + assert.sameValue(u.searchParams, u.searchParams); +} + +let myURL; + +// Hash +myURL = new URL("https://example.org/foo#bar"); +myURL.hash = "baz"; +assert.sameValue(myURL.href, "https://example.org/foo#baz"); + +myURL.hash = "#baz"; +assert.sameValue(myURL.href, "https://example.org/foo#baz"); + +myURL.hash = "#á=1 2"; +assert.sameValue(myURL.href, "https://example.org/foo#%C3%A1=1%202"); + +myURL.hash = "#a/#b"; +// FAILING: the second # gets escaped +//assert.sameValue(myURL.href, "https://example.org/foo#a/#b"); +assert.sameValue(myURL.search, ""); +// FAILING: the second # gets escaped +//assert.sameValue(myURL.hash, "#a/#b"); + +// Host +myURL = new URL("https://example.org:81/foo"); +myURL.host = "example.com:82"; +assert.sameValue(myURL.href, "https://example.com:82/foo"); + +// Hostname +myURL = new URL("https://example.org:81/foo"); +myURL.hostname = "example.com:82"; +assert.sameValue(myURL.href, "https://example.org:81/foo"); + +myURL.hostname = "á.com"; +assert.sameValue(myURL.href, "https://xn--1ca.com:81/foo"); + +// href +myURL = new URL("https://example.org/foo"); +myURL.href = "https://example.com/bar"; +assert.sameValue(myURL.href, "https://example.com/bar"); + +// Password +myURL = new URL("https://abc:xyz@example.com"); +myURL.password = "123"; +assert.sameValue(myURL.href, "https://abc:123@example.com/"); + +// pathname +myURL = new URL("https://example.org/abc/xyz?123"); +myURL.pathname = "/abcdef"; +assert.sameValue(myURL.href, "https://example.org/abcdef?123"); +assert.sameValue(myURL.toString(), "https://example.org/abcdef?123"); + +myURL.pathname = ""; +assert.sameValue(myURL.href, "https://example.org/?123"); + +myURL.pathname = "á"; +assert.sameValue(myURL.pathname, "/%C3%A1"); +assert.sameValue(myURL.href, "https://example.org/%C3%A1?123"); + +myURL = new URL("file:///./abc"); +assert.sameValue(myURL.pathname, "/abc"); + +myURL = new URL("fish://host/."); +assert.sameValue(myURL.pathname, "/"); +myURL.pathname = "."; +assert.sameValue(myURL.pathname, "/"); + +myURL = new URL("fish://host/a/../b"); +assert.sameValue(myURL.pathname, '/b'); +myURL.pathname = 'a/../c'; +assert.sameValue(myURL.pathname, '/c'); + +myURL = new URL("file://"); +assert.sameValue(myURL.pathname, "/"); +assert.sameValue(myURL.href, "file:///"); + +assert.throwsNodeError(() => { + new URL("http://"); +}, TypeError, "ERR_INVALID_URL"); + +// myURL = new URL("fish://"); +// assert.sameValue(myURL.pathname, ""); +// Currently returns "fish:" +// assert.sameValue(myURL.href, "fish://"); + +// port + +myURL = new URL("https://example.org:8888"); +assert.sameValue(myURL.port, "8888"); + +function testSetPort(port, expected) { + const url = new URL("https://example.org:8888"); + url.port = port; + assert.sameValue(url.port, expected); +} + +testSetPort(0, "0"); +testSetPort(-0, "0"); + +// Default ports are automatically transformed to the empty string +// (HTTPS protocol's default port is 443) +testSetPort("443", ""); +testSetPort(443, ""); + +// Empty string is the same as default port +testSetPort("", ""); + +// Completely invalid port strings are ignored +testSetPort("abcd", "8888"); +testSetPort("-123", ""); +testSetPort(-123, ""); +testSetPort(-123.45, ""); +testSetPort(undefined, "8888"); +testSetPort(null, "8888"); +testSetPort(+Infinity, "8888"); +testSetPort(-Infinity, "8888"); +testSetPort(NaN, "8888"); + +// Leading numbers are treated as a port number +testSetPort("5678abcd", "5678"); +testSetPort("a5678abcd", ""); + +// Non-integers are truncated +testSetPort(1234.5678, "1234"); + +// Out-of-range numbers which are not represented in scientific notation +// will be ignored. +testSetPort(1e10, "8888"); +testSetPort("123456", "8888"); +testSetPort(123456, "8888"); +testSetPort(4.567e21, "4"); + +// toString() takes precedence over valueOf(), even if it returns a valid integer +testSetPort( + { + toString() { + return "2"; + }, + valueOf() { + return 1; + }, + }, + "2" +); + +// Protocol +function testSetProtocol(url, protocol, expected) { + url.protocol = protocol; + assert.sameValue(url.protocol, expected); +} +testSetProtocol(new URL("https://example.org"), "ftp", "ftp:"); +testSetProtocol(new URL("https://example.org"), "ftp:", "ftp:"); +testSetProtocol(new URL("https://example.org"), "FTP:", "ftp:"); +testSetProtocol(new URL("https://example.org"), "ftp: blah", "ftp:"); +// special to non-special +testSetProtocol(new URL("https://example.org"), "foo", "https:"); +// non-special to special +testSetProtocol(new URL("fish://example.org"), "https", "fish:"); + +// Search +myURL = new URL("https://example.org/abc?123"); +myURL.search = "abc=xyz"; +assert.sameValue(myURL.href, "https://example.org/abc?abc=xyz"); + +myURL.search = "a=1 2"; +assert.sameValue(myURL.href, "https://example.org/abc?a=1%202"); + +myURL.search = "á=ú"; +assert.sameValue(myURL.search, "?%C3%A1=%C3%BA"); +assert.sameValue(myURL.href, "https://example.org/abc?%C3%A1=%C3%BA"); + +myURL.hash = "hash"; +myURL.search = "a=#b"; +assert.sameValue(myURL.href, "https://example.org/abc?a=%23b#hash"); +assert.sameValue(myURL.search, "?a=%23b"); +assert.sameValue(myURL.hash, "#hash"); + +// Username +myURL = new URL("https://abc:xyz@example.com/"); +myURL.username = "123"; +assert.sameValue(myURL.href, "https://123:xyz@example.com/"); + +// Origin, read-only +assert.throws(() => { + myURL.origin = "abc"; +}, TypeError); + +// href +myURL = new URL("https://example.org"); +myURL.href = "https://example.com"; +assert.sameValue(myURL.href, "https://example.com/"); + +assert.throws(() => { + myURL.href = "test"; +}, TypeError); + +// Search Params +myURL = new URL("https://example.com/"); +myURL.searchParams.append("user", "abc"); +assert.sameValue(myURL.toString(), "https://example.com/?user=abc"); +myURL.searchParams.append("first", "one"); +assert.sameValue(myURL.toString(), "https://example.com/?user=abc&first=one"); +myURL.searchParams.delete("user"); +assert.sameValue(myURL.toString(), "https://example.com/?first=one"); + +{ + const url = require("url"); + + assert.sameValue(url.domainToASCII('español.com'), "xn--espaol-zwa.com"); + assert.sameValue(url.domainToASCII('中文.com'), "xn--fiq228c.com"); + assert.sameValue(url.domainToASCII('xn--iñvalid.com'), ""); + + assert.sameValue(url.domainToUnicode('xn--espaol-zwa.com'), "español.com"); + assert.sameValue(url.domainToUnicode('xn--fiq228c.com'), "中文.com"); + assert.sameValue(url.domainToUnicode('xn--iñvalid.com'), ""); +} + +{ + const url = new URL("otpauth://totp"); + url.pathname = 'domain.com Domain:user@domain.com'; + assert.sameValue(url.toString(), 'otpauth://totp/domain.com%20Domain:user@domain.com'); +} diff --git a/url/types/README.md b/url/types/README.md new file mode 100644 index 0000000..118268b --- /dev/null +++ b/url/types/README.md @@ -0,0 +1,10 @@ +## Type definitions for the goja_nodejs url module. + +This package contains type definitions which only include features +currently implemented by the goja_nodejs url module. + +### Install + +```shell +npm install --save-dev @dop251/types-goja_nodejs-url +``` diff --git a/url/types/package.json b/url/types/package.json new file mode 100644 index 0000000..797745b --- /dev/null +++ b/url/types/package.json @@ -0,0 +1,19 @@ +{ + "name": "@dop251/types-goja_nodejs-url", + "version": "0.0.1-rc2", + "types": "url.d.ts", + "scripts": { + "test": "tsc --noEmit" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/dop251/goja_nodejs.git" + }, + "dependencies": { + "@dop251/types-goja_nodejs-global": "0.0.1-rc2" + }, + "devDependencies": { + "typescript": "next" + }, + "private": false +} diff --git a/url/types/tsconfig.json b/url/types/tsconfig.json new file mode 100644 index 0000000..6d09a23 --- /dev/null +++ b/url/types/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "esnext", + "lib": [ + "es6", + "dom" + ], + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noEmit": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/url/types/url.d.ts b/url/types/url.d.ts new file mode 100644 index 0000000..b7509ab --- /dev/null +++ b/url/types/url.d.ts @@ -0,0 +1,543 @@ +/// +declare module 'url' { + /** + * Returns the [Punycode](https://tools.ietf.org/html/rfc5891#section-4.4) ASCII serialization of the `domain`. If `domain` is an + * invalid domain, the empty string is returned. + * + * It performs the inverse operation to {@link domainToUnicode}. + * + * ```js + * import url from 'node:url'; + * + * console.log(url.domainToASCII('español.com')); + * // Prints xn--espaol-zwa.com + * console.log(url.domainToASCII('中文.com')); + * // Prints xn--fiq228c.com + * console.log(url.domainToASCII('xn--iñvalid.com')); + * // Prints an empty string + * ``` + * @since v7.4.0, v6.13.0 + */ + function domainToASCII(domain: string): string; + /** + * Returns the Unicode serialization of the `domain`. If `domain` is an invalid + * domain, the empty string is returned. + * + * It performs the inverse operation to {@link domainToASCII}. + * + * ```js + * import url from 'node:url'; + * + * console.log(url.domainToUnicode('xn--espaol-zwa.com')); + * // Prints español.com + * console.log(url.domainToUnicode('xn--fiq228c.com')); + * // Prints 中文.com + * console.log(url.domainToUnicode('xn--iñvalid.com')); + * // Prints an empty string + * ``` + * @since v7.4.0, v6.13.0 + */ + function domainToUnicode(domain: string): string; + + /** + * Browser-compatible `URL` class, implemented by following the WHATWG URL + * Standard. [Examples of parsed URLs](https://url.spec.whatwg.org/#example-url-parsing) may be found in the Standard itself. + * The `URL` class is also available on the global object. + * + * In accordance with browser conventions, all properties of `URL` objects + * are implemented as getters and setters on the class prototype, rather than as + * data properties on the object itself. Thus, unlike `legacy urlObject`s, + * using the `delete` keyword on any properties of `URL` objects (e.g. `delete myURL.protocol`, `delete myURL.pathname`, etc) has no effect but will still + * return `true`. + * @since v7.0.0, v6.13.0 + */ + class URL { + constructor(input: string | { toString: () => string }, base?: string | URL); + /** + * Gets and sets the fragment portion of the URL. + * + * ```js + * const myURL = new URL('https://example.org/foo#bar'); + * console.log(myURL.hash); + * // Prints #bar + * + * myURL.hash = 'baz'; + * console.log(myURL.href); + * // Prints https://example.org/foo#baz + * ``` + * + * Invalid URL characters included in the value assigned to the `hash` property + * are `percent-encoded`. The selection of which characters to + * percent-encode may vary somewhat from what the {@link parse} and {@link format} methods would produce. + */ + hash: string; + /** + * Gets and sets the host portion of the URL. + * + * ```js + * const myURL = new URL('https://example.org:81/foo'); + * console.log(myURL.host); + * // Prints example.org:81 + * + * myURL.host = 'example.com:82'; + * console.log(myURL.href); + * // Prints https://example.com:82/foo + * ``` + * + * Invalid host values assigned to the `host` property are ignored. + */ + host: string; + /** + * Gets and sets the host name portion of the URL. The key difference between`url.host` and `url.hostname` is that `url.hostname` does _not_ include the + * port. + * + * ```js + * const myURL = new URL('https://example.org:81/foo'); + * console.log(myURL.hostname); + * // Prints example.org + * + * // Setting the hostname does not change the port + * myURL.hostname = 'example.com'; + * console.log(myURL.href); + * // Prints https://example.com:81/foo + * + * // Use myURL.host to change the hostname and port + * myURL.host = 'example.org:82'; + * console.log(myURL.href); + * // Prints https://example.org:82/foo + * ``` + * + * Invalid host name values assigned to the `hostname` property are ignored. + */ + hostname: string; + /** + * Gets and sets the serialized URL. + * + * ```js + * const myURL = new URL('https://example.org/foo'); + * console.log(myURL.href); + * // Prints https://example.org/foo + * + * myURL.href = 'https://example.com/bar'; + * console.log(myURL.href); + * // Prints https://example.com/bar + * ``` + * + * Getting the value of the `href` property is equivalent to calling {@link toString}. + * + * Setting the value of this property to a new value is equivalent to creating a + * new `URL` object using `new URL(value)`. Each of the `URL` object's properties will be modified. + * + * If the value assigned to the `href` property is not a valid URL, a `TypeError` will be thrown. + */ + href: string; + /** + * Gets the read-only serialization of the URL's origin. + * + * ```js + * const myURL = new URL('https://example.org/foo/bar?baz'); + * console.log(myURL.origin); + * // Prints https://example.org + * ``` + * + * ```js + * const idnURL = new URL('https://測試'); + * console.log(idnURL.origin); + * // Prints https://xn--g6w251d + * + * console.log(idnURL.hostname); + * // Prints xn--g6w251d + * ``` + */ + readonly origin: string; + /** + * Gets and sets the password portion of the URL. + * + * ```js + * const myURL = new URL('https://abc:xyz@example.com'); + * console.log(myURL.password); + * // Prints xyz + * + * myURL.password = '123'; + * console.log(myURL.href); + * // Prints https://abc:123@example.com/ + * ``` + * + * Invalid URL characters included in the value assigned to the `password` property + * are `percent-encoded`. The selection of which characters to + * percent-encode may vary somewhat from what the {@link parse} and {@link format} methods would produce. + */ + password: string; + /** + * Gets and sets the path portion of the URL. + * + * ```js + * const myURL = new URL('https://example.org/abc/xyz?123'); + * console.log(myURL.pathname); + * // Prints /abc/xyz + * + * myURL.pathname = '/abcdef'; + * console.log(myURL.href); + * // Prints https://example.org/abcdef?123 + * ``` + * + * Invalid URL characters included in the value assigned to the `pathname` property are `percent-encoded`. The selection of which characters + * to percent-encode may vary somewhat from what the {@link parse} and {@link format} methods would produce. + */ + pathname: string; + /** + * Gets and sets the port portion of the URL. + * + * The port value may be a number or a string containing a number in the range `0` to `65535` (inclusive). Setting the value to the default port of the `URL` objects given `protocol` will + * result in the `port` value becoming + * the empty string (`''`). + * + * The port value can be an empty string in which case the port depends on + * the protocol/scheme: + * + * + * + * Upon assigning a value to the port, the value will first be converted to a + * string using `.toString()`. + * + * If that string is invalid but it begins with a number, the leading number is + * assigned to `port`. + * If the number lies outside the range denoted above, it is ignored. + * + * ```js + * const myURL = new URL('https://example.org:8888'); + * console.log(myURL.port); + * // Prints 8888 + * + * // Default ports are automatically transformed to the empty string + * // (HTTPS protocol's default port is 443) + * myURL.port = '443'; + * console.log(myURL.port); + * // Prints the empty string + * console.log(myURL.href); + * // Prints https://example.org/ + * + * myURL.port = 1234; + * console.log(myURL.port); + * // Prints 1234 + * console.log(myURL.href); + * // Prints https://example.org:1234/ + * + * // Completely invalid port strings are ignored + * myURL.port = 'abcd'; + * console.log(myURL.port); + * // Prints 1234 + * + * // Leading numbers are treated as a port number + * myURL.port = '5678abcd'; + * console.log(myURL.port); + * // Prints 5678 + * + * // Non-integers are truncated + * myURL.port = 1234.5678; + * console.log(myURL.port); + * // Prints 1234 + * + * // Out-of-range numbers which are not represented in scientific notation + * // will be ignored. + * myURL.port = 1e10; // 10000000000, will be range-checked as described below + * console.log(myURL.port); + * // Prints 1234 + * ``` + * + * Numbers which contain a decimal point, + * such as floating-point numbers or numbers in scientific notation, + * are not an exception to this rule. + * Leading numbers up to the decimal point will be set as the URL's port, + * assuming they are valid: + * + * ```js + * myURL.port = 4.567e21; + * console.log(myURL.port); + * // Prints 4 (because it is the leading number in the string '4.567e21') + * ``` + */ + port: string; + /** + * Gets and sets the protocol portion of the URL. + * + * ```js + * const myURL = new URL('https://example.org'); + * console.log(myURL.protocol); + * // Prints https: + * + * myURL.protocol = 'ftp'; + * console.log(myURL.href); + * // Prints ftp://example.org/ + * ``` + * + * Invalid URL protocol values assigned to the `protocol` property are ignored. + */ + protocol: string; + /** + * Gets and sets the serialized query portion of the URL. + * + * ```js + * const myURL = new URL('https://example.org/abc?123'); + * console.log(myURL.search); + * // Prints ?123 + * + * myURL.search = 'abc=xyz'; + * console.log(myURL.href); + * // Prints https://example.org/abc?abc=xyz + * ``` + * + * Any invalid URL characters appearing in the value assigned the `search` property will be `percent-encoded`. The selection of which + * characters to percent-encode may vary somewhat from what the {@link parse} and {@link format} methods would produce. + */ + search: string; + /** + * Gets the `URLSearchParams` object representing the query parameters of the + * URL. This property is read-only but the `URLSearchParams` object it provides + * can be used to mutate the URL instance; to replace the entirety of query + * parameters of the URL, use the {@link search} setter. See `URLSearchParams` documentation for details. + * + * Use care when using `.searchParams` to modify the `URL` because, + * per the WHATWG specification, the `URLSearchParams` object uses + * different rules to determine which characters to percent-encode. For + * instance, the `URL` object will not percent encode the ASCII tilde (`~`) + * character, while `URLSearchParams` will always encode it: + * + * ```js + * const myURL = new URL('https://example.org/abc?foo=~bar'); + * + * console.log(myURL.search); // prints ?foo=~bar + * + * // Modify the URL via searchParams... + * myURL.searchParams.sort(); + * + * console.log(myURL.search); // prints ?foo=%7Ebar + * ``` + */ + readonly searchParams: URLSearchParams; + /** + * Gets and sets the username portion of the URL. + * + * ```js + * const myURL = new URL('https://abc:xyz@example.com'); + * console.log(myURL.username); + * // Prints abc + * + * myURL.username = '123'; + * console.log(myURL.href); + * // Prints https://123:xyz@example.com/ + * ``` + * + * Any invalid URL characters appearing in the value assigned the `username` property will be `percent-encoded`. The selection of which + * characters to percent-encode may vary somewhat from what the {@link parse} and {@link format} methods would produce. + */ + username: string; + /** + * The `toString()` method on the `URL` object returns the serialized URL. The + * value returned is equivalent to that of {@link href} and {@link toJSON}. + */ + toString(): string; + /** + * The `toJSON()` method on the `URL` object returns the serialized URL. The + * value returned is equivalent to that of {@link href} and {@link toString}. + * + * This method is automatically called when an `URL` object is serialized + * with [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify). + * + * ```js + * const myURLs = [ + * new URL('https://www.example.com'), + * new URL('https://test.example.org'), + * ]; + * console.log(JSON.stringify(myURLs)); + * // Prints ["https://www.example.com/","https://test.example.org/"] + * ``` + */ + toJSON(): string; + } + interface URLSearchParamsIterator extends GojaNodeJS.Iterator { + [Symbol.iterator](): URLSearchParamsIterator; + } + + /** + * The `URLSearchParams` API provides read and write access to the query of a `URL`. The `URLSearchParams` class can also be used standalone with one of the + * four following constructors. + * The `URLSearchParams` class is also available on the global object. + * + * The WHATWG `URLSearchParams` interface and the `querystring` module have + * similar purpose, but the purpose of the `querystring` module is more + * general, as it allows the customization of delimiter characters (`&` and `=`). + * On the other hand, this API is designed purely for URL query strings. + * + * ```js + * const myURL = new URL('https://example.org/?abc=123'); + * console.log(myURL.searchParams.get('abc')); + * // Prints 123 + * + * myURL.searchParams.append('abc', 'xyz'); + * console.log(myURL.href); + * // Prints https://example.org/?abc=123&abc=xyz + * + * myURL.searchParams.delete('abc'); + * myURL.searchParams.set('a', 'b'); + * console.log(myURL.href); + * // Prints https://example.org/?a=b + * + * const newSearchParams = new URLSearchParams(myURL.searchParams); + * // The above is equivalent to + * // const newSearchParams = new URLSearchParams(myURL.search); + * + * newSearchParams.append('a', 'c'); + * console.log(myURL.href); + * // Prints https://example.org/?a=b + * console.log(newSearchParams.toString()); + * // Prints a=b&a=c + * + * // newSearchParams.toString() is implicitly called + * myURL.search = newSearchParams; + * console.log(myURL.href); + * // Prints https://example.org/?a=b&a=c + * newSearchParams.delete('a'); + * console.log(myURL.href); + * // Prints https://example.org/?a=b&a=c + * ``` + * @since v7.5.0, v6.13.0 + */ + class URLSearchParams implements Iterable<[string, string]> { + constructor( + init?: + | URLSearchParams + | string + | Record + | Iterable<[string, string]> + | ReadonlyArray<[string, string]>, + ); + /** + * Append a new name-value pair to the query string. + */ + append(name: string, value: string): void; + /** + * If `value` is provided, removes all name-value pairs + * where name is `name` and value is `value`. + * + * If `value` is not provided, removes all name-value pairs whose name is `name`. + */ + delete(name: string, value?: string): void; + /** + * Returns an ES6 `Iterator` over each of the name-value pairs in the query. + * Each item of the iterator is a JavaScript `Array`. The first item of the `Array` is the `name`, the second item of the `Array` is the `value`. + * + * Alias for `urlSearchParams[@@iterator]()`. + */ + entries(): URLSearchParamsIterator<[string, string]>; + /** + * Iterates over each name-value pair in the query and invokes the given function. + * + * ```js + * const myURL = new URL('https://example.org/?a=b&c=d'); + * myURL.searchParams.forEach((value, name, searchParams) => { + * console.log(name, value, myURL.searchParams === searchParams); + * }); + * // Prints: + * // a b true + * // c d true + * ``` + * @param fn Invoked for each name-value pair in the query + * @param thisArg To be used as `this` value for when `fn` is called + */ + forEach( + fn: (this: TThis, value: string, name: string, searchParams: URLSearchParams) => void, + thisArg?: TThis, + ): void; + /** + * Returns the value of the first name-value pair whose name is `name`. If there + * are no such pairs, `null` is returned. + * @return or `null` if there is no name-value pair with the given `name`. + */ + get(name: string): string | null; + /** + * Returns the values of all name-value pairs whose name is `name`. If there are + * no such pairs, an empty array is returned. + */ + getAll(name: string): string[]; + /** + * Checks if the `URLSearchParams` object contains key-value pair(s) based on `name` and an optional `value` argument. + * + * If `value` is provided, returns `true` when name-value pair with + * same `name` and `value` exists. + * + * If `value` is not provided, returns `true` if there is at least one name-value + * pair whose name is `name`. + */ + has(name: string, value?: string): boolean; + /** + * Returns an ES6 `Iterator` over the names of each name-value pair. + * + * ```js + * const params = new URLSearchParams('foo=bar&foo=baz'); + * for (const name of params.keys()) { + * console.log(name); + * } + * // Prints: + * // foo + * // foo + * ``` + */ + keys(): URLSearchParamsIterator; + /** + * Sets the value in the `URLSearchParams` object associated with `name` to `value`. If there are any pre-existing name-value pairs whose names are `name`, + * set the first such pair's value to `value` and remove all others. If not, + * append the name-value pair to the query string. + * + * ```js + * const params = new URLSearchParams(); + * params.append('foo', 'bar'); + * params.append('foo', 'baz'); + * params.append('abc', 'def'); + * console.log(params.toString()); + * // Prints foo=bar&foo=baz&abc=def + * + * params.set('foo', 'def'); + * params.set('xyz', 'opq'); + * console.log(params.toString()); + * // Prints foo=def&abc=def&xyz=opq + * ``` + */ + set(name: string, value: string): void; + /** + * The total number of parameter entries. + * @since v19.8.0 + */ + readonly size: number; + /** + * Sort all existing name-value pairs in-place by their names. Sorting is done + * with a [stable sorting algorithm](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability), so relative order between name-value pairs + * with the same name is preserved. + * + * This method can be used, in particular, to increase cache hits. + * + * ```js + * const params = new URLSearchParams('query[]=abc&type=search&query[]=123'); + * params.sort(); + * console.log(params.toString()); + * // Prints query%5B%5D=abc&query%5B%5D=123&type=search + * ``` + * @since v7.7.0, v6.13.0 + */ + sort(): void; + /** + * Returns the search parameters serialized as a string, with characters + * percent-encoded where necessary. + */ + toString(): string; + /** + * Returns an ES6 `Iterator` over the values of each name-value pair. + */ + values(): URLSearchParamsIterator; + [Symbol.iterator](): URLSearchParamsIterator<[string, string]>; + } +} + +declare module "node:url" { + export * from "url"; +} diff --git a/url/url.go b/url/url.go new file mode 100644 index 0000000..0de4760 --- /dev/null +++ b/url/url.go @@ -0,0 +1,417 @@ +package url + +import ( + "math" + "net/url" + "path" + "reflect" + "strconv" + "strings" + + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/errors" + + "golang.org/x/net/idna" +) + +const ( + URLNotAbsolute = "URL is not absolute" + InvalidURL = "Invalid URL" + InvalidBaseURL = "Invalid base URL" + InvalidHostname = "Invalid hostname" +) + +var ( + reflectTypeURL = reflect.TypeOf((*nodeURL)(nil)) + reflectTypeInt = reflect.TypeOf(int64(0)) +) + +func toURL(r *goja.Runtime, v goja.Value) *nodeURL { + if v.ExportType() == reflectTypeURL { + if u := v.Export().(*nodeURL); u != nil { + return u + } + } + + panic(errors.NewTypeError(r, errors.ErrCodeInvalidThis, `Value of "this" must be of type URL`)) +} + +func (m *urlModule) newInvalidURLError(msg, input string) *goja.Object { + o := errors.NewTypeError(m.r, "ERR_INVALID_URL", msg) + o.Set("input", m.r.ToValue(input)) + return o +} + +func (m *urlModule) defineURLAccessorProp(p *goja.Object, name string, getter func(*nodeURL) interface{}, setter func(*nodeURL, goja.Value)) { + var getterVal, setterVal goja.Value + if getter != nil { + getterVal = m.r.ToValue(func(call goja.FunctionCall) goja.Value { + return m.r.ToValue(getter(toURL(m.r, call.This))) + }) + } + if setter != nil { + setterVal = m.r.ToValue(func(call goja.FunctionCall) goja.Value { + setter(toURL(m.r, call.This), call.Argument(0)) + return goja.Undefined() + }) + } + p.DefineAccessorProperty(name, getterVal, setterVal, goja.FLAG_FALSE, goja.FLAG_TRUE) +} + +func valueToURLPort(v goja.Value) (portNum int, empty bool) { + portNum = -1 + if et := v.ExportType(); et == reflectTypeInt { + num := v.ToInteger() + if num < 0 { + empty = true + } else if num <= math.MaxUint16 { + portNum = int(num) + } + } else { + s := v.String() + if s == "" { + return 0, true + } + firstDigitIdx := -1 + for i := 0; i < len(s); i++ { + if c := s[i]; c >= '0' && c <= '9' { + firstDigitIdx = i + break + } + } + + if firstDigitIdx == -1 { + return -1, false + } + + if firstDigitIdx > 0 { + return 0, true + } + + for i := 0; i < len(s); i++ { + if c := s[i]; c >= '0' && c <= '9' { + if portNum == -1 { + portNum = 0 + } + portNum = portNum*10 + int(c-'0') + if portNum > math.MaxUint16 { + portNum = -1 + break + } + } else { + break + } + } + } + return +} + +func isDefaultURLPort(protocol string, port int) bool { + switch port { + case 21: + if protocol == "ftp" { + return true + } + case 80: + if protocol == "http" || protocol == "ws" { + return true + } + case 443: + if protocol == "https" || protocol == "wss" { + return true + } + } + return false +} + +func isSpecialProtocol(protocol string) bool { + switch protocol { + case "ftp", "file", "http", "https", "ws", "wss": + return true + } + return false +} + +func isSpecialNetProtocol(protocol string) bool { + switch protocol { + case "https", "http", "ftp", "wss", "ws": + return true + } + return false +} + +func clearURLPort(u *url.URL) { + u.Host = u.Hostname() +} + +func setURLPort(nu *nodeURL, v goja.Value) { + u := nu.url + if u.Scheme == "file" { + return + } + portNum, empty := valueToURLPort(v) + if empty { + clearURLPort(u) + return + } + if portNum == -1 { + return + } + if isDefaultURLPort(u.Scheme, portNum) { + clearURLPort(u) + } else { + u.Host = u.Hostname() + ":" + strconv.Itoa(portNum) + } +} + +func (m *urlModule) parseURL(s string, isBase bool) *url.URL { + u, err := url.Parse(s) + if err != nil { + if isBase { + panic(m.newInvalidURLError(InvalidBaseURL, s)) + } else { + panic(m.newInvalidURLError(InvalidURL, s)) + } + } + if isBase && !u.IsAbs() { + panic(m.newInvalidURLError(URLNotAbsolute, s)) + } + if isSpecialNetProtocol(u.Scheme) && u.Host == "" && u.Path == "" { + panic(m.newInvalidURLError(InvalidURL, s)) + } + if portStr := u.Port(); portStr != "" { + if port, err := strconv.Atoi(portStr); err != nil || isDefaultURLPort(u.Scheme, port) { + u.Host = u.Hostname() // Clear port + } + } + m.fixURL(u) + return u +} + +func fixRawQuery(u *url.URL) { + if u.RawQuery != "" { + u.RawQuery = escape(u.RawQuery, &tblEscapeURLQuery, false) + } +} + +func cleanPath(p, proto string) string { + if !strings.HasPrefix(p, "/") && (isSpecialProtocol(proto) || p != "") { + p = "/" + p + } + if p != "" { + return path.Clean(p) + } + return "" +} + +func (m *urlModule) fixURL(u *url.URL) { + u.Path = cleanPath(u.Path, u.Scheme) + if isSpecialNetProtocol(u.Scheme) { + hostname := u.Hostname() + lh := strings.ToLower(hostname) + ch, err := idna.Punycode.ToASCII(lh) + if err != nil { + panic(m.newInvalidURLError(InvalidHostname, lh)) + } + if ch != hostname { + if port := u.Port(); port != "" { + u.Host = ch + ":" + port + } else { + u.Host = ch + } + } + } + fixRawQuery(u) +} + +func (m *urlModule) createURLPrototype() *goja.Object { + p := m.r.NewObject() + + // host + m.defineURLAccessorProp(p, "host", func(u *nodeURL) interface{} { + return u.url.Host + }, func(u *nodeURL, arg goja.Value) { + host := arg.String() + if _, err := url.ParseRequestURI(u.url.Scheme + "://" + host); err == nil { + u.url.Host = host + m.fixURL(u.url) + } + }) + + // hash + m.defineURLAccessorProp(p, "hash", func(u *nodeURL) interface{} { + if u.url.Fragment != "" { + return "#" + u.url.EscapedFragment() + } + return "" + }, func(u *nodeURL, arg goja.Value) { + h := arg.String() + if len(h) > 0 && h[0] == '#' { + h = h[1:] + } + u.url.Fragment = h + }) + + // hostname + m.defineURLAccessorProp(p, "hostname", func(u *nodeURL) interface{} { + return strings.Split(u.url.Host, ":")[0] + }, func(u *nodeURL, arg goja.Value) { + h := arg.String() + if strings.IndexByte(h, ':') >= 0 { + return + } + if _, err := url.ParseRequestURI(u.url.Scheme + "://" + h); err == nil { + if port := u.url.Port(); port != "" { + u.url.Host = h + ":" + port + } else { + u.url.Host = h + } + m.fixURL(u.url) + } + }) + + // href + m.defineURLAccessorProp(p, "href", func(u *nodeURL) interface{} { + return u.String() + }, func(u *nodeURL, arg goja.Value) { + u.url = m.parseURL(arg.String(), true) + }) + + // pathname + m.defineURLAccessorProp(p, "pathname", func(u *nodeURL) interface{} { + return u.url.EscapedPath() + }, func(u *nodeURL, arg goja.Value) { + u.url.Path = cleanPath(arg.String(), u.url.Scheme) + }) + + // origin + m.defineURLAccessorProp(p, "origin", func(u *nodeURL) interface{} { + return u.url.Scheme + "://" + u.url.Hostname() + }, nil) + + // password + m.defineURLAccessorProp(p, "password", func(u *nodeURL) interface{} { + p, _ := u.url.User.Password() + return p + }, func(u *nodeURL, arg goja.Value) { + user := u.url.User + u.url.User = url.UserPassword(user.Username(), arg.String()) + }) + + // username + m.defineURLAccessorProp(p, "username", func(u *nodeURL) interface{} { + return u.url.User.Username() + }, func(u *nodeURL, arg goja.Value) { + p, has := u.url.User.Password() + if !has { + u.url.User = url.User(arg.String()) + } else { + u.url.User = url.UserPassword(arg.String(), p) + } + }) + + // port + m.defineURLAccessorProp(p, "port", func(u *nodeURL) interface{} { + return u.url.Port() + }, func(u *nodeURL, arg goja.Value) { + setURLPort(u, arg) + }) + + // protocol + m.defineURLAccessorProp(p, "protocol", func(u *nodeURL) interface{} { + return u.url.Scheme + ":" + }, func(u *nodeURL, arg goja.Value) { + s := arg.String() + pos := strings.IndexByte(s, ':') + if pos >= 0 { + s = s[:pos] + } + s = strings.ToLower(s) + if isSpecialProtocol(u.url.Scheme) == isSpecialProtocol(s) { + if _, err := url.ParseRequestURI(s + "://" + u.url.Host); err == nil { + u.url.Scheme = s + } + } + }) + + // Search + m.defineURLAccessorProp(p, "search", func(u *nodeURL) interface{} { + u.syncSearchParams() + if u.url.RawQuery != "" { + return "?" + u.url.RawQuery + } + return "" + }, func(u *nodeURL, arg goja.Value) { + u.url.RawQuery = arg.String() + fixRawQuery(u.url) + if u.searchParams != nil { + u.searchParams = parseSearchQuery(u.url.RawQuery) + if u.searchParams == nil { + u.searchParams = make(searchParams, 0) + } + } + }) + + // search Params + m.defineURLAccessorProp(p, "searchParams", func(u *nodeURL) interface{} { + if u.searchParams == nil { + sp := parseSearchQuery(u.url.RawQuery) + if sp == nil { + sp = make(searchParams, 0) + } + u.searchParams = sp + } + return m.newURLSearchParams((*urlSearchParams)(u)) + }, nil) + + p.Set("toString", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toURL(m.r, call.This) + u.syncSearchParams() + return m.r.ToValue(u.url.String()) + })) + + p.Set("toJSON", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toURL(m.r, call.This) + u.syncSearchParams() + return m.r.ToValue(u.url.String()) + })) + + return p +} + +func (m *urlModule) createURLConstructor() goja.Value { + f := m.r.ToValue(func(call goja.ConstructorCall) *goja.Object { + var u *url.URL + if baseArg := call.Argument(1); !goja.IsUndefined(baseArg) { + base := m.parseURL(baseArg.String(), true) + ref := m.parseURL(call.Argument(0).String(), false) + u = base.ResolveReference(ref) + } else { + u = m.parseURL(call.Argument(0).String(), true) + } + res := m.r.ToValue(&nodeURL{url: u}).(*goja.Object) + res.SetPrototype(call.This.Prototype()) + return res + }).(*goja.Object) + + proto := m.createURLPrototype() + f.Set("prototype", proto) + proto.DefineDataProperty("constructor", f, goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_FALSE) + return f +} + +func (m *urlModule) domainToASCII(domUnicode string) string { + res, err := idna.ToASCII(domUnicode) + if err != nil { + return "" + } + return res +} + +func (m *urlModule) domainToUnicode(domASCII string) string { + res, err := idna.ToUnicode(domASCII) + if err != nil { + return "" + } + return res +} diff --git a/url/url_test.go b/url/url_test.go new file mode 100644 index 0000000..862922b --- /dev/null +++ b/url/url_test.go @@ -0,0 +1,122 @@ +package url + +import ( + _ "embed" + "testing" + + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/require" +) + +func TestURL(t *testing.T) { + vm := goja.New() + new(require.Registry).Enable(vm) + Enable(vm) + + if c := vm.Get("URL"); c == nil { + t.Fatal("URL not found") + } + + script := `const url = new URL("https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash");` + + if _, err := vm.RunString(script); err != nil { + t.Fatal("Failed to process url script.", err) + } +} + +func TestGetters(t *testing.T) { + vm := goja.New() + new(require.Registry).Enable(vm) + Enable(vm) + + if c := vm.Get("URL"); c == nil { + t.Fatal("URL not found") + } + + script := ` + new URL("https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hashed"); + ` + + v, err := vm.RunString(script) + if err != nil { + t.Fatal("Failed to process url script.", err) + } + + url := v.ToObject(vm) + + tests := []struct { + prop string + expected string + }{ + { + prop: "hash", + expected: "#hashed", + }, + { + prop: "host", + expected: "sub.example.com:8080", + }, + { + prop: "hostname", + expected: "sub.example.com", + }, + { + prop: "href", + expected: "https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hashed", + }, + { + prop: "origin", + expected: "https://sub.example.com", + }, + { + prop: "password", + expected: "pass", + }, + { + prop: "username", + expected: "user", + }, + { + prop: "port", + expected: "8080", + }, + { + prop: "protocol", + expected: "https:", + }, + { + prop: "search", + expected: "?query=string", + }, + } + + for _, test := range tests { + v := url.Get(test.prop).String() + if v != test.expected { + t.Fatal("failed to match " + test.prop + " property. got: " + v + ", expected: " + test.expected) + } + } +} + +//go:embed testdata/url_test.js +var urlTest string + +func TestJs(t *testing.T) { + vm := goja.New() + new(require.Registry).Enable(vm) + Enable(vm) + + if c := vm.Get("URL"); c == nil { + t.Fatal("URL not found") + } + + // Script will throw an error on failed validation + + _, err := vm.RunScript("testdata/url_test.js", urlTest) + if err != nil { + if ex, ok := err.(*goja.Exception); ok { + t.Fatal(ex.String()) + } + t.Fatal("Failed to process url script.", err) + } +} diff --git a/url/urlsearchparams.go b/url/urlsearchparams.go new file mode 100644 index 0000000..aa73416 --- /dev/null +++ b/url/urlsearchparams.go @@ -0,0 +1,408 @@ +package url + +import ( + "reflect" + "sort" + + "github.com/dop251/goja_nodejs/errors" + + "github.com/dop251/goja" +) + +var ( + reflectTypeURLSearchParams = reflect.TypeOf((*urlSearchParams)(nil)) + reflectTypeURLSearchParamsIterator = reflect.TypeOf((*urlSearchParamsIterator)(nil)) +) + +func newInvalidTupleError(r *goja.Runtime) *goja.Object { + return errors.NewTypeError(r, "ERR_INVALID_TUPLE", "Each query pair must be an iterable [name, value] tuple") +} + +func newMissingArgsError(r *goja.Runtime, msg string) *goja.Object { + return errors.NewTypeError(r, errors.ErrCodeMissingArgs, msg) +} + +func newInvalidCallbackTypeError(r *goja.Runtime) *goja.Object { + return errors.NewNotCorrectTypeError(r, "callback", "function") +} + +func toUrlSearchParams(r *goja.Runtime, v goja.Value) *urlSearchParams { + if v.ExportType() == reflectTypeURLSearchParams { + if u := v.Export().(*urlSearchParams); u != nil { + return u + } + } + panic(errors.NewTypeError(r, errors.ErrCodeInvalidThis, `Value of "this" must be of type URLSearchParams`)) +} + +func (m *urlModule) newURLSearchParams(sp *urlSearchParams) *goja.Object { + v := m.r.ToValue(sp).(*goja.Object) + v.SetPrototype(m.URLSearchParamsPrototype) + return v +} + +func (m *urlModule) createURLSearchParamsConstructor() goja.Value { + f := m.r.ToValue(func(call goja.ConstructorCall) *goja.Object { + var sp searchParams + v := call.Argument(0) + if o, ok := v.(*goja.Object); ok { + sp = m.buildParamsFromObject(o) + } else if !goja.IsUndefined(v) { + sp = parseSearchQuery(v.String()) + } + + return m.newURLSearchParams(&urlSearchParams{searchParams: sp}) + }).(*goja.Object) + + m.URLSearchParamsPrototype = m.createURLSearchParamsPrototype() + f.Set("prototype", m.URLSearchParamsPrototype) + m.URLSearchParamsPrototype.DefineDataProperty("constructor", f, goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_FALSE) + + return f +} + +func (m *urlModule) buildParamsFromObject(o *goja.Object) searchParams { + var query searchParams + + if o.GetSymbol(goja.SymIterator) != nil { + return m.buildParamsFromIterable(o) + } + + for _, k := range o.Keys() { + val := o.Get(k).String() + query = append(query, searchParam{name: k, value: val}) + } + + return query +} + +func (m *urlModule) buildParamsFromIterable(o *goja.Object) searchParams { + var query searchParams + + m.r.ForOf(o, func(val goja.Value) bool { + obj := val.ToObject(m.r) + var name, value string + i := 0 + // Use ForOf to determine if the object is iterable + m.r.ForOf(obj, func(val goja.Value) bool { + if i == 0 { + name = val.String() + i++ + return true + } + if i == 1 { + value = val.String() + i++ + return true + } + // Array isn't a tuple + panic(newInvalidTupleError(m.r)) + }) + + // Ensure we have two values + if i <= 1 { + panic(newInvalidTupleError(m.r)) + } + + query = append(query, searchParam{ + name: name, + value: value, + }) + + return true + }) + + return query +} + +func (m *urlModule) createURLSearchParamsPrototype() *goja.Object { + p := m.r.NewObject() + + p.Set("append", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + if len(call.Arguments) < 2 { + panic(newMissingArgsError(m.r, `The "name" and "value" arguments must be specified`)) + } + + u := toUrlSearchParams(m.r, call.This) + u.searchParams = append(u.searchParams, searchParam{ + name: call.Argument(0).String(), + value: call.Argument(1).String(), + }) + u.markUpdated() + + return goja.Undefined() + })) + + p.Set("delete", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toUrlSearchParams(m.r, call.This) + + if len(call.Arguments) < 1 { + panic(newMissingArgsError(m.r, `The "name" argument must be specified`)) + } + + name := call.Argument(0).String() + isValid := func(v searchParam) bool { + if len(call.Arguments) == 1 { + return v.name != name + } else if v.name == name { + arg := call.Argument(1) + if !goja.IsUndefined(arg) && v.value == arg.String() { + return false + } + } + return true + } + + j := 0 + for i, v := range u.searchParams { + if isValid(v) { + if i != j { + u.searchParams[j] = v + } + j++ + } + } + u.searchParams = u.searchParams[:j] + u.markUpdated() + + return goja.Undefined() + })) + + entries := m.r.ToValue(func(call goja.FunctionCall) goja.Value { + return m.newURLSearchParamsIterator(toUrlSearchParams(m.r, call.This), urlSearchParamsIteratorEntries) + }) + p.Set("entries", entries) + p.DefineDataPropertySymbol(goja.SymIterator, entries, goja.FLAG_TRUE, goja.FLAG_FALSE, goja.FLAG_TRUE) + + p.Set("forEach", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toUrlSearchParams(m.r, call.This) + + if fn, ok := goja.AssertFunction(call.Argument(0)); ok { + for _, pair := range u.searchParams { + // name, value, searchParams + _, err := fn( + nil, + m.r.ToValue(pair.name), + m.r.ToValue(pair.value), + call.This, + ) + + if err != nil { + panic(err) + } + } + } else { + panic(newInvalidCallbackTypeError(m.r)) + } + + return goja.Undefined() + })) + + p.Set("get", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toUrlSearchParams(m.r, call.This) + + if len(call.Arguments) == 0 { + panic(newMissingArgsError(m.r, `The "name" argument must be specified`)) + } + + if val, exists := u.getFirstValue(call.Argument(0).String()); exists { + return m.r.ToValue(val) + } + + return goja.Null() + })) + + p.Set("getAll", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toUrlSearchParams(m.r, call.This) + + if len(call.Arguments) == 0 { + panic(newMissingArgsError(m.r, `The "name" argument must be specified`)) + } + + vals := u.getValues(call.Argument(0).String()) + return m.r.ToValue(vals) + })) + + p.Set("has", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toUrlSearchParams(m.r, call.This) + + if len(call.Arguments) == 0 { + panic(newMissingArgsError(m.r, `The "name" argument must be specified`)) + } + + name := call.Argument(0).String() + value := call.Argument(1) + var res bool + if goja.IsUndefined(value) { + res = u.hasName(name) + } else { + res = u.hasValue(name, value.String()) + } + return m.r.ToValue(res) + })) + + p.Set("keys", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + return m.newURLSearchParamsIterator(toUrlSearchParams(m.r, call.This), urlSearchParamsIteratorKeys) + })) + + p.Set("set", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toUrlSearchParams(m.r, call.This) + + if len(call.Arguments) < 2 { + panic(newMissingArgsError(m.r, `The "name" and "value" arguments must be specified`)) + } + + name := call.Argument(0).String() + found := false + j := 0 + for i, sp := range u.searchParams { + if sp.name == name { + if found { + continue // Remove all values + } + + u.searchParams[i].value = call.Argument(1).String() + found = true + } + if i != j { + u.searchParams[j] = sp + } + j++ + } + + if !found { + u.searchParams = append(u.searchParams, searchParam{ + name: name, + value: call.Argument(1).String(), + }) + } else { + u.searchParams = u.searchParams[:j] + } + + u.markUpdated() + + return goja.Undefined() + })) + + p.Set("sort", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toUrlSearchParams(m.r, call.This) + sort.Stable(u.searchParams) + u.markUpdated() + return goja.Undefined() + })) + + p.DefineAccessorProperty("size", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toUrlSearchParams(m.r, call.This) + return m.r.ToValue(len(u.searchParams)) + }), nil, goja.FLAG_FALSE, goja.FLAG_TRUE) + + p.Set("toString", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + u := toUrlSearchParams(m.r, call.This) + str := u.searchParams.Encode() + return m.r.ToValue(str) + })) + + p.Set("values", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + return m.newURLSearchParamsIterator(toUrlSearchParams(m.r, call.This), urlSearchParamsIteratorValues) + })) + + return p +} + +func (sp *urlSearchParams) markUpdated() { + if sp.url != nil && sp.url.RawQuery != "" { + sp.url.RawQuery = "" + } +} + +type urlSearchParamsIteratorType int + +const ( + urlSearchParamsIteratorKeys urlSearchParamsIteratorType = iota + urlSearchParamsIteratorValues + urlSearchParamsIteratorEntries +) + +type urlSearchParamsIterator struct { + typ urlSearchParamsIteratorType + sp *urlSearchParams + idx int +} + +func toURLSearchParamsIterator(r *goja.Runtime, v goja.Value) *urlSearchParamsIterator { + if v.ExportType() == reflectTypeURLSearchParamsIterator { + if u := v.Export().(*urlSearchParamsIterator); u != nil { + return u + } + } + + panic(errors.NewTypeError(r, errors.ErrCodeInvalidThis, `Value of "this" must be of type URLSearchParamIterator`)) +} + +func getIteratorPrototype(r *goja.Runtime) (iteratorProto *goja.Object) { + ar := r.NewArray() + if fn, ok := goja.AssertFunction(ar.GetSymbol(goja.SymIterator)); ok { + iter, err := fn(ar) + if err != nil { + panic(err) + } + iteratorProto = iter.ToObject(r).Prototype() + if iteratorProto == nil { + panic(r.NewTypeError("[][Symbol.iterator().__proto__ is null")) + } + iteratorProto = iteratorProto.Prototype() + if iteratorProto == nil { + panic(r.NewTypeError("[][Symbol.iterator().__proto__.__proto__ is null")) + } + } else { + panic(r.NewTypeError("[][Symbol.iterator is not a function")) + } + return +} + +func (m *urlModule) getURLSearchParamsIteratorPrototype() *goja.Object { + if m.URLSearchParamsIteratorPrototype != nil { + return m.URLSearchParamsIteratorPrototype + } + + p := m.r.NewObject() + p.SetPrototype(getIteratorPrototype(m.r)) + + p.Set("next", m.r.ToValue(func(call goja.FunctionCall) goja.Value { + it := toURLSearchParamsIterator(m.r, call.This) + res := m.r.NewObject() + if it.idx < len(it.sp.searchParams) { + param := it.sp.searchParams[it.idx] + switch it.typ { + case urlSearchParamsIteratorKeys: + res.Set("value", param.name) + case urlSearchParamsIteratorValues: + res.Set("value", param.value) + default: + res.Set("value", m.r.NewArray(param.name, param.value)) + } + res.Set("done", false) + it.idx++ + } else { + res.Set("value", goja.Undefined()) + res.Set("done", true) + } + return res + })) + + p.DefineDataPropertySymbol(goja.SymToStringTag, m.r.ToValue("URLSearchParams Iterator"), goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_TRUE) + + m.URLSearchParamsIteratorPrototype = p + return p +} + +func (m *urlModule) newURLSearchParamsIterator(sp *urlSearchParams, typ urlSearchParamsIteratorType) goja.Value { + it := m.r.ToValue(&urlSearchParamsIterator{ + typ: typ, + sp: sp, + }).(*goja.Object) + + it.SetPrototype(m.getURLSearchParamsIteratorPrototype()) + + return it +} diff --git a/url/urlsearchparams_test.go b/url/urlsearchparams_test.go new file mode 100644 index 0000000..62f5b6a --- /dev/null +++ b/url/urlsearchparams_test.go @@ -0,0 +1,53 @@ +package url + +import ( + _ "embed" + "testing" + + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/console" + "github.com/dop251/goja_nodejs/require" +) + +func createVM() *goja.Runtime { + vm := goja.New() + new(require.Registry).Enable(vm) + console.Enable(vm) + Enable(vm) + return vm +} + +func TestURLSearchParams(t *testing.T) { + vm := createVM() + + if c := vm.Get("URLSearchParams"); c == nil { + t.Fatal("URLSearchParams not found") + } + + script := `const params = new URLSearchParams();` + + if _, err := vm.RunString(script); err != nil { + t.Fatal("Failed to process url script.", err) + } +} + +//go:embed testdata/url_search_params.js +var url_search_params string + +func TestURLSearchParameters(t *testing.T) { + vm := createVM() + + if c := vm.Get("URLSearchParams"); c == nil { + t.Fatal("URLSearchParams not found") + } + + // Script will throw an error on failed validation + + _, err := vm.RunScript("testdata/url_search_params.js", url_search_params) + if err != nil { + if ex, ok := err.(*goja.Exception); ok { + t.Fatal(ex.String()) + } + t.Fatal("Failed to process url script.", err) + } +} diff --git a/util/module.go b/util/module.go new file mode 100644 index 0000000..0f1927c --- /dev/null +++ b/util/module.go @@ -0,0 +1,104 @@ +package util + +import ( + "bytes" + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/require" +) + +const ModuleName = "util" + +type Util struct { + runtime *goja.Runtime +} + +func (u *Util) format(f rune, val goja.Value, w *bytes.Buffer) bool { + switch f { + case 's': + w.WriteString(val.String()) + case 'd': + w.WriteString(val.ToNumber().String()) + case 'j': + if json, ok := u.runtime.Get("JSON").(*goja.Object); ok { + if stringify, ok := goja.AssertFunction(json.Get("stringify")); ok { + res, err := stringify(json, val) + if err != nil { + panic(err) + } + w.WriteString(res.String()) + } + } + case '%': + w.WriteByte('%') + return false + default: + w.WriteByte('%') + w.WriteRune(f) + return false + } + return true +} + +func (u *Util) Format(b *bytes.Buffer, f string, args ...goja.Value) { + pct := false + argNum := 0 + for _, chr := range f { + if pct { + if argNum < len(args) { + if u.format(chr, args[argNum], b) { + argNum++ + } + } else { + b.WriteByte('%') + b.WriteRune(chr) + } + pct = false + } else { + if chr == '%' { + pct = true + } else { + b.WriteRune(chr) + } + } + } + + for _, arg := range args[argNum:] { + b.WriteByte(' ') + b.WriteString(arg.String()) + } +} + +func (u *Util) js_format(call goja.FunctionCall) goja.Value { + var b bytes.Buffer + var fmt string + + if arg := call.Argument(0); !goja.IsUndefined(arg) { + fmt = arg.String() + } + + var args []goja.Value + if len(call.Arguments) > 0 { + args = call.Arguments[1:] + } + u.Format(&b, fmt, args...) + + return u.runtime.ToValue(b.String()) +} + +func Require(runtime *goja.Runtime, module *goja.Object) { + u := &Util{ + runtime: runtime, + } + obj := module.Get("exports").(*goja.Object) + obj.Set("format", u.js_format) +} + +func New(runtime *goja.Runtime) *Util { + return &Util{ + runtime: runtime, + } +} + +func init() { + require.RegisterCoreModule(ModuleName, Require) +} diff --git a/util/module_test.go b/util/module_test.go new file mode 100644 index 0000000..b95ca84 --- /dev/null +++ b/util/module_test.go @@ -0,0 +1,74 @@ +package util + +import ( + "bytes" + "testing" + + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/require" +) + +func TestUtil_Format(t *testing.T) { + vm := goja.New() + util := New(vm) + + var b bytes.Buffer + util.Format(&b, "Test: %% %д %s %d, %j", vm.ToValue("string"), vm.ToValue(42), vm.NewObject()) + + if res := b.String(); res != "Test: % %д string 42, {}" { + t.Fatalf("Unexpected result: '%s'", res) + } +} + +func TestUtil_Format_NoArgs(t *testing.T) { + vm := goja.New() + util := New(vm) + + var b bytes.Buffer + util.Format(&b, "Test: %s %d, %j") + + if res := b.String(); res != "Test: %s %d, %j" { + t.Fatalf("Unexpected result: '%s'", res) + } +} + +func TestUtil_Format_LessArgs(t *testing.T) { + vm := goja.New() + util := New(vm) + + var b bytes.Buffer + util.Format(&b, "Test: %s %d, %j", vm.ToValue("string"), vm.ToValue(42)) + + if res := b.String(); res != "Test: string 42, %j" { + t.Fatalf("Unexpected result: '%s'", res) + } +} + +func TestUtil_Format_MoreArgs(t *testing.T) { + vm := goja.New() + util := New(vm) + + var b bytes.Buffer + util.Format(&b, "Test: %s %d, %j", vm.ToValue("string"), vm.ToValue(42), vm.NewObject(), vm.ToValue(42.42)) + + if res := b.String(); res != "Test: string 42, {} 42.42" { + t.Fatalf("Unexpected result: '%s'", res) + } +} + +func TestJSNoArgs(t *testing.T) { + vm := goja.New() + new(require.Registry).Enable(vm) + + if util, ok := require.Require(vm, ModuleName).(*goja.Object); ok { + if format, ok := goja.AssertFunction(util.Get("format")); ok { + res, err := format(util) + if err != nil { + t.Fatal(err) + } + if v := res.Export(); v != "" { + t.Fatalf("Unexpected result: %v", v) + } + } + } +}