Skip to content

Commit 7f96e64

Browse files
committed
Improved URLSearchParams implementation.
1 parent 76fdc05 commit 7f96e64

File tree

9 files changed

+825
-357
lines changed

9 files changed

+825
-357
lines changed

assert.js

+20
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,26 @@ const assert = {
5757
return;
5858
}
5959
throw new Error(message + "No exception was thrown");
60+
},
61+
62+
throwsNodeError(f, ctor, code, message) {
63+
if (message === undefined) {
64+
message = '';
65+
} else {
66+
message += ' ';
67+
}
68+
try {
69+
f();
70+
} catch (e) {
71+
if (e.constructor !== ctor) {
72+
throw new Error(message + "Wrong exception type was thrown: " + e.constructor.name);
73+
}
74+
if (e.code !== code) {
75+
throw new Error(message + "Wrong exception code was thrown: " + e.code);
76+
}
77+
return;
78+
}
79+
throw new Error(message + "No exception was thrown");
6080
}
6181
}
6282

errors/errors.go

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88

99
const (
1010
ErrCodeInvalidArgType = "ERR_INVALID_ARG_TYPE"
11+
ErrCodeInvalidThis = "ERR_INVALID_THIS"
12+
ErrCodeMissingArgs = "ERR_MISSING_ARGS"
1113
)
1214

1315
func error_toString(call goja.FunctionCall, r *goja.Runtime) goja.Value {

url/escape.go

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package url
2+
3+
import "strings"
4+
5+
var tblEscapeURLQuery = [128]byte{
6+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
7+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
8+
0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1,
9+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1,
10+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
11+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
12+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
13+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
14+
}
15+
16+
var tblEscapeURLQueryParam = [128]byte{
17+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
18+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
19+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0,
20+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
21+
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
22+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1,
23+
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
24+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
25+
}
26+
27+
// The code below is mostly borrowed from the standard Go url package
28+
29+
const upperhex = "0123456789ABCDEF"
30+
31+
func ishex(c byte) bool {
32+
switch {
33+
case '0' <= c && c <= '9':
34+
return true
35+
case 'a' <= c && c <= 'f':
36+
return true
37+
case 'A' <= c && c <= 'F':
38+
return true
39+
}
40+
return false
41+
}
42+
43+
func unhex(c byte) byte {
44+
switch {
45+
case '0' <= c && c <= '9':
46+
return c - '0'
47+
case 'a' <= c && c <= 'f':
48+
return c - 'a' + 10
49+
case 'A' <= c && c <= 'F':
50+
return c - 'A' + 10
51+
}
52+
return 0
53+
}
54+
55+
func escape(s string, table *[128]byte, spaceToPlus bool) string {
56+
spaceCount, hexCount := 0, 0
57+
for i := 0; i < len(s); i++ {
58+
c := s[i]
59+
if c > 127 || table[c] == 0 {
60+
if c == ' ' && spaceToPlus {
61+
spaceCount++
62+
} else {
63+
hexCount++
64+
}
65+
}
66+
}
67+
68+
if spaceCount == 0 && hexCount == 0 {
69+
return s
70+
}
71+
72+
var sb strings.Builder
73+
hexBuf := [3]byte{'%', 0, 0}
74+
75+
sb.Grow(len(s) + 2*hexCount)
76+
77+
for i := 0; i < len(s); i++ {
78+
switch c := s[i]; {
79+
case c == ' ' && spaceToPlus:
80+
sb.WriteByte('+')
81+
case c > 127 || table[c] == 0:
82+
hexBuf[1] = upperhex[c>>4]
83+
hexBuf[2] = upperhex[c&15]
84+
sb.Write(hexBuf[:])
85+
default:
86+
sb.WriteByte(c)
87+
}
88+
}
89+
return sb.String()
90+
}
91+
92+
func unescapeSearchParam(s string) string {
93+
n := 0
94+
hasPlus := false
95+
for i := 0; i < len(s); {
96+
switch s[i] {
97+
case '%':
98+
if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {
99+
i++
100+
continue
101+
}
102+
n++
103+
i += 3
104+
case '+':
105+
hasPlus = true
106+
i++
107+
default:
108+
i++
109+
}
110+
}
111+
112+
if n == 0 && !hasPlus {
113+
return s
114+
}
115+
116+
var t strings.Builder
117+
t.Grow(len(s) - 2*n)
118+
for i := 0; i < len(s); i++ {
119+
switch s[i] {
120+
case '%':
121+
if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {
122+
t.WriteByte('%')
123+
} else {
124+
t.WriteByte(unhex(s[i+1])<<4 | unhex(s[i+2]))
125+
i += 2
126+
}
127+
case '+':
128+
t.WriteByte(' ')
129+
default:
130+
t.WriteByte(s[i])
131+
}
132+
}
133+
return t.String()
134+
}

url/module.go

+9-24
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,20 @@ import (
77

88
const ModuleName = "url"
99

10-
func toURL(r *goja.Runtime, v goja.Value) *nodeURL {
11-
if v.ExportType() == reflectTypeURL {
12-
if u := v.Export().(*nodeURL); u != nil {
13-
return u
14-
}
15-
}
16-
panic(r.NewTypeError("Expected URL"))
17-
}
10+
type urlModule struct {
11+
r *goja.Runtime
1812

19-
func defineURLAccessorProp(r *goja.Runtime, p *goja.Object, name string, getter func(*nodeURL) interface{}, setter func(*nodeURL, goja.Value)) {
20-
var getterVal, setterVal goja.Value
21-
if getter != nil {
22-
getterVal = r.ToValue(func(call goja.FunctionCall) goja.Value {
23-
return r.ToValue(getter(toURL(r, call.This)))
24-
})
25-
}
26-
if setter != nil {
27-
setterVal = r.ToValue(func(call goja.FunctionCall) goja.Value {
28-
setter(toURL(r, call.This), call.Argument(0))
29-
return goja.Undefined()
30-
})
31-
}
32-
p.DefineAccessorProperty(name, getterVal, setterVal, goja.FLAG_FALSE, goja.FLAG_TRUE)
13+
URLSearchParamsPrototype *goja.Object
14+
URLSearchParamsIteratorPrototype *goja.Object
3315
}
3416

3517
func Require(runtime *goja.Runtime, module *goja.Object) {
3618
exports := module.Get("exports").(*goja.Object)
37-
exports.Set("URL", createURLConstructor(runtime))
38-
exports.Set("URLSearchParams", createURLSearchParamsConstructor(runtime))
19+
m := &urlModule{
20+
r: runtime,
21+
}
22+
exports.Set("URL", m.createURLConstructor())
23+
exports.Set("URLSearchParams", m.createURLSearchParamsConstructor())
3924
}
4025

4126
func Enable(runtime *goja.Runtime) {

url/nodeurl.go

+62-29
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package url
22

33
import (
4-
"fmt"
54
"net/url"
65
"strings"
76
)
@@ -15,11 +14,15 @@ func (sp *searchParam) Encode() string {
1514
return sp.string(true)
1615
}
1716

17+
func escapeSearchParam(s string) string {
18+
return escape(s, &tblEscapeURLQueryParam, true)
19+
}
20+
1821
func (sp *searchParam) string(encode bool) string {
1922
if encode {
20-
return fmt.Sprintf("%s=%s", url.QueryEscape(sp.name), url.QueryEscape(sp.value))
23+
return escapeSearchParam(sp.name) + "=" + escapeSearchParam(sp.value)
2124
} else {
22-
return fmt.Sprintf("%s=%s", sp.name, sp.value)
25+
return sp.name + "=" + sp.value
2326
}
2427
}
2528

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

3639
func (s searchParams) Less(i, j int) bool {
37-
return len(s[i].name) > len(s[j].name)
40+
return strings.Compare(s[i].name, s[j].name) < 0
3841
}
3942

4043
func (s searchParams) Encode() string {
41-
str := ""
42-
sep := ""
43-
for _, v := range s {
44-
str = fmt.Sprintf("%s%s%s", str, sep, v.Encode())
45-
sep = "&"
44+
var sb strings.Builder
45+
for i, v := range s {
46+
if i > 0 {
47+
sb.WriteByte('&')
48+
}
49+
sb.WriteString(v.Encode())
4650
}
47-
return str
51+
return sb.String()
4852
}
4953

5054
func (s searchParams) String() string {
51-
var b strings.Builder
52-
sep := ""
53-
for _, v := range s {
54-
b.WriteString(sep)
55-
b.WriteString(v.string(false)) // keep it raw
56-
sep = "&"
55+
var sb strings.Builder
56+
for i, v := range s {
57+
if i > 0 {
58+
sb.WriteByte('&')
59+
}
60+
sb.WriteString(v.string(false))
5761
}
58-
return b.String()
62+
return sb.String()
5963
}
6064

6165
type nodeURL struct {
6266
url *url.URL
6367
searchParams searchParams
6468
}
6569

70+
type urlSearchParams nodeURL
71+
6672
// This methods ensures that the url.URL has the proper RawQuery based on the searchParam
6773
// structs. If a change is made to the searchParams we need to keep them in sync.
6874
func (nu *nodeURL) syncSearchParams() {
69-
nu.url.RawQuery = nu.searchParams.Encode()
75+
if nu.rawQueryUpdateNeeded() {
76+
nu.url.RawQuery = nu.searchParams.Encode()
77+
}
78+
}
79+
80+
func (nu *nodeURL) rawQueryUpdateNeeded() bool {
81+
return len(nu.searchParams) > 0 && nu.url.RawQuery == ""
7082
}
7183

7284
func (nu *nodeURL) String() string {
7385
return nu.url.String()
7486
}
7587

76-
func (nu *nodeURL) hasName(name string) bool {
77-
for _, v := range nu.searchParams {
88+
func (sp *urlSearchParams) hasName(name string) bool {
89+
for _, v := range sp.searchParams {
7890
if v.name == name {
7991
return true
8092
}
8193
}
8294
return false
8395
}
8496

85-
func (nu *nodeURL) getValues(name string) []string {
86-
var vals []string
87-
for _, v := range nu.searchParams {
97+
func (sp *urlSearchParams) hasValue(name, value string) bool {
98+
for _, v := range sp.searchParams {
99+
if v.name == name && v.value == value {
100+
return true
101+
}
102+
}
103+
return false
104+
}
105+
106+
func (sp *urlSearchParams) getValues(name string) []string {
107+
vals := make([]string, 0, len(sp.searchParams))
108+
for _, v := range sp.searchParams {
88109
if v.name == name {
89110
vals = append(vals, v.value)
90111
}
@@ -93,23 +114,35 @@ func (nu *nodeURL) getValues(name string) []string {
93114
return vals
94115
}
95116

96-
func parseSearchQuery(query string) searchParams {
97-
ret := searchParams{}
117+
func (sp *urlSearchParams) getFirstValue(name string) (string, bool) {
118+
for _, v := range sp.searchParams {
119+
if v.name == name {
120+
return v.value, true
121+
}
122+
}
123+
124+
return "", false
125+
}
126+
127+
func parseSearchQuery(query string) (ret searchParams) {
98128
if query == "" {
99-
return ret
129+
return
100130
}
101131

102132
query = strings.TrimPrefix(query, "?")
103133

104134
for _, v := range strings.Split(query, "&") {
135+
if v == "" {
136+
continue
137+
}
105138
pair := strings.SplitN(v, "=", 2)
106139
l := len(pair)
107140
if l == 1 {
108-
ret = append(ret, searchParam{name: pair[0], value: ""})
141+
ret = append(ret, searchParam{name: unescapeSearchParam(pair[0]), value: ""})
109142
} else if l == 2 {
110-
ret = append(ret, searchParam{name: pair[0], value: pair[1]})
143+
ret = append(ret, searchParam{name: unescapeSearchParam(pair[0]), value: unescapeSearchParam(pair[1])})
111144
}
112145
}
113146

114-
return ret
147+
return
115148
}

0 commit comments

Comments
 (0)