Skip to content

Commit

Permalink
Improved URLSearchParams implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
dop251 committed Sep 13, 2023
1 parent 76fdc05 commit 7f96e64
Show file tree
Hide file tree
Showing 9 changed files with 825 additions and 357 deletions.
20 changes: 20 additions & 0 deletions assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,26 @@ const assert = {
return;
}
throw new Error(message + "No exception was thrown");
},

throwsNodeError(f, ctor, code, message) {
if (message === undefined) {
message = '';
} else {
message += ' ';
}
try {
f();
} catch (e) {
if (e.constructor !== ctor) {
throw new Error(message + "Wrong exception type was thrown: " + e.constructor.name);
}
if (e.code !== code) {
throw new Error(message + "Wrong exception code was thrown: " + e.code);
}
return;
}
throw new Error(message + "No exception was thrown");
}
}

Expand Down
2 changes: 2 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (

const (
ErrCodeInvalidArgType = "ERR_INVALID_ARG_TYPE"
ErrCodeInvalidThis = "ERR_INVALID_THIS"
ErrCodeMissingArgs = "ERR_MISSING_ARGS"
)

func error_toString(call goja.FunctionCall, r *goja.Runtime) goja.Value {
Expand Down
134 changes: 134 additions & 0 deletions url/escape.go
Original file line number Diff line number Diff line change
@@ -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()
}
33 changes: 9 additions & 24 deletions url/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,20 @@ import (

const ModuleName = "url"

func toURL(r *goja.Runtime, v goja.Value) *nodeURL {
if v.ExportType() == reflectTypeURL {
if u := v.Export().(*nodeURL); u != nil {
return u
}
}
panic(r.NewTypeError("Expected URL"))
}
type urlModule struct {
r *goja.Runtime

func defineURLAccessorProp(r *goja.Runtime, p *goja.Object, name string, getter func(*nodeURL) interface{}, setter func(*nodeURL, goja.Value)) {
var getterVal, setterVal goja.Value
if getter != nil {
getterVal = r.ToValue(func(call goja.FunctionCall) goja.Value {
return r.ToValue(getter(toURL(r, call.This)))
})
}
if setter != nil {
setterVal = r.ToValue(func(call goja.FunctionCall) goja.Value {
setter(toURL(r, call.This), call.Argument(0))
return goja.Undefined()
})
}
p.DefineAccessorProperty(name, getterVal, setterVal, goja.FLAG_FALSE, goja.FLAG_TRUE)
URLSearchParamsPrototype *goja.Object
URLSearchParamsIteratorPrototype *goja.Object
}

func Require(runtime *goja.Runtime, module *goja.Object) {
exports := module.Get("exports").(*goja.Object)
exports.Set("URL", createURLConstructor(runtime))
exports.Set("URLSearchParams", createURLSearchParamsConstructor(runtime))
m := &urlModule{
r: runtime,
}
exports.Set("URL", m.createURLConstructor())
exports.Set("URLSearchParams", m.createURLSearchParamsConstructor())
}

func Enable(runtime *goja.Runtime) {
Expand Down
91 changes: 62 additions & 29 deletions url/nodeurl.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package url

import (
"fmt"
"net/url"
"strings"
)
Expand All @@ -15,11 +14,15 @@ 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 fmt.Sprintf("%s=%s", url.QueryEscape(sp.name), url.QueryEscape(sp.value))
return escapeSearchParam(sp.name) + "=" + escapeSearchParam(sp.value)
} else {
return fmt.Sprintf("%s=%s", sp.name, sp.value)
return sp.name + "=" + sp.value
}
}

Expand All @@ -34,57 +37,75 @@ func (s searchParams) Swap(i, j int) {
}

func (s searchParams) Less(i, j int) bool {
return len(s[i].name) > len(s[j].name)
return strings.Compare(s[i].name, s[j].name) < 0
}

func (s searchParams) Encode() string {
str := ""
sep := ""
for _, v := range s {
str = fmt.Sprintf("%s%s%s", str, sep, v.Encode())
sep = "&"
var sb strings.Builder
for i, v := range s {
if i > 0 {
sb.WriteByte('&')
}
sb.WriteString(v.Encode())
}
return str
return sb.String()
}

func (s searchParams) String() string {
var b strings.Builder
sep := ""
for _, v := range s {
b.WriteString(sep)
b.WriteString(v.string(false)) // keep it raw
sep = "&"
var sb strings.Builder
for i, v := range s {
if i > 0 {
sb.WriteByte('&')
}
sb.WriteString(v.string(false))
}
return b.String()
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() {
nu.url.RawQuery = nu.searchParams.Encode()
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 (nu *nodeURL) hasName(name string) bool {
for _, v := range nu.searchParams {
func (sp *urlSearchParams) hasName(name string) bool {
for _, v := range sp.searchParams {
if v.name == name {
return true
}
}
return false
}

func (nu *nodeURL) getValues(name string) []string {
var vals []string
for _, v := range nu.searchParams {
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)
}
Expand All @@ -93,23 +114,35 @@ func (nu *nodeURL) getValues(name string) []string {
return vals
}

func parseSearchQuery(query string) searchParams {
ret := searchParams{}
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 ret
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: pair[0], value: ""})
ret = append(ret, searchParam{name: unescapeSearchParam(pair[0]), value: ""})
} else if l == 2 {
ret = append(ret, searchParam{name: pair[0], value: pair[1]})
ret = append(ret, searchParam{name: unescapeSearchParam(pair[0]), value: unescapeSearchParam(pair[1])})
}
}

return ret
return
}
Loading

0 comments on commit 7f96e64

Please sign in to comment.