Skip to content

Commit 66c9f58

Browse files
committed
Merge remote-tracking branch 'dop251/master' into updateGoja
2 parents 54f7abb + 8b74ce4 commit 66c9f58

15 files changed

+314
-26
lines changed

builtin_array.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,40 @@ func (r *Runtime) arrayproto_pop(call FunctionCall) Value {
176176
}
177177
}
178178

179+
// pushToStringStack checks for circular references and pushes an object onto the toString stack.
180+
// Returns true if the object is already in the stack (circular reference detected), false otherwise.
181+
// If false is returned, the caller must ensure the object is popped from the stack when done.
182+
func (r *Runtime) pushToStringStack(o *Object) bool {
183+
// Check for circular reference in the toString stack
184+
for _, obj := range r.toStringStack {
185+
if o == obj {
186+
// Circular reference detected
187+
return true
188+
}
189+
}
190+
191+
// Push this object onto the stack
192+
r.toStringStack = append(r.toStringStack, o)
193+
return false
194+
}
195+
196+
// popFromStringStack removes an object from the toString stack.
197+
func (r *Runtime) popFromStringStack() {
198+
// Set the last element to nil to allow GC to collect it
199+
r.toStringStack[len(r.toStringStack)-1] = nil
200+
r.toStringStack = r.toStringStack[:len(r.toStringStack)-1]
201+
}
202+
179203
func (r *Runtime) arrayproto_join(call FunctionCall) Value {
180204
o := call.This.ToObject(r)
205+
206+
if r.pushToStringStack(o) {
207+
// Circular reference detected, return empty string to avoid infinite recursion
208+
// This matches the behavior of mainstream JavaScript engines (V8, SpiderMonkey)
209+
return stringEmpty
210+
}
211+
defer r.popFromStringStack()
212+
181213
l := int(toLength(o.self.getStr("length", nil)))
182214
var sep String
183215
if s := call.Argument(0); s != _undefined {
@@ -249,6 +281,13 @@ func (r *Runtime) writeItemLocaleString(item Value, buf *StringBuilder) {
249281

250282
func (r *Runtime) arrayproto_toLocaleString(call FunctionCall) Value {
251283
array := call.This.ToObject(r)
284+
285+
if r.pushToStringStack(array) {
286+
// Circular reference detected, return empty string to avoid infinite recursion
287+
return stringEmpty
288+
}
289+
defer r.popFromStringStack()
290+
252291
var buf StringBuilder
253292
if a := r.checkStdArrayObj(array); a != nil {
254293
for i, item := range a.values {

builtin_array_circular_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package sobek
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestArrayCircularReferenceToString(t *testing.T) {
8+
const SCRIPT = `
9+
var T = [1, 2, 3];
10+
T[42] = T; // Create circular reference
11+
var str = String(T);
12+
// Circular reference should be replaced with empty string
13+
str === "1,2,3,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,";
14+
`
15+
testScript(SCRIPT, valueTrue, t)
16+
}
17+
18+
func TestArrayCircularReferenceNumericOperation(t *testing.T) {
19+
const SCRIPT = `
20+
var T = [1, 2, 3];
21+
T[42] = T; // Create circular reference
22+
try {
23+
var x = T % 2; // This should not crash
24+
true;
25+
} catch (e) {
26+
false;
27+
}
28+
`
29+
testScript(SCRIPT, valueTrue, t)
30+
}
31+
32+
func TestArrayCircularReferenceJoin(t *testing.T) {
33+
const SCRIPT = `
34+
var T = [1, 2, 3];
35+
T[42] = T; // Create circular reference
36+
var str = T.join(',');
37+
// Circular reference should be replaced with empty string
38+
str === "1,2,3,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,";
39+
`
40+
testScript(SCRIPT, valueTrue, t)
41+
}
42+
43+
func TestArrayCircularReferenceConcat(t *testing.T) {
44+
const SCRIPT = `
45+
var T = [1, 2, 3];
46+
T[42] = T; // Create circular reference
47+
var str = '' + T; // String concatenation
48+
// Circular reference should be replaced with empty string
49+
str === "1,2,3,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,";
50+
`
51+
testScript(SCRIPT, valueTrue, t)
52+
}
53+
54+
func TestArrayCircularReferenceToLocaleString(t *testing.T) {
55+
const SCRIPT = `
56+
var T = [1, 2, 3];
57+
T[42] = T; // Create circular reference
58+
var str = T.toLocaleString();
59+
// Circular reference should be replaced with empty string
60+
str === "1,2,3,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,";
61+
`
62+
testScript(SCRIPT, valueTrue, t)
63+
}
64+
65+
func TestArrayMultipleCircularReferences(t *testing.T) {
66+
const SCRIPT = `
67+
var T = [1, 2, 3];
68+
T[42] = T;
69+
T[76] = T;
70+
T[80] = T;
71+
var str = String(T);
72+
// Should handle multiple circular references - all should be empty strings
73+
str.split(',').length === 81;
74+
`
75+
testScript(SCRIPT, valueTrue, t)
76+
}
77+
78+
func TestArrayNestedCircularReference(t *testing.T) {
79+
const SCRIPT = `
80+
var A = [1, 2];
81+
var B = [3, 4];
82+
A[2] = B;
83+
B[2] = A; // Mutual circular reference
84+
var str = String(A);
85+
// A contains B which contains A - circular refs should be empty
86+
str === "1,2,3,4,";
87+
`
88+
testScript(SCRIPT, valueTrue, t)
89+
}
90+
91+
func TestArrayCircularReferenceAccessOK(t *testing.T) {
92+
const SCRIPT = `
93+
// These operations should still work fine
94+
var T = [1, 2, 3];
95+
T[42] = T;
96+
97+
// Accessing circular reference is OK
98+
var same = T[42] === T;
99+
100+
// Accessing elements through circular reference is OK
101+
var first = T[42][0];
102+
103+
// Deep nesting is OK
104+
var deep = T[42][42][42][42][42][0];
105+
106+
same && first === 1 && deep === 1;
107+
`
108+
testScript(SCRIPT, valueTrue, t)
109+
}
110+
111+
func TestArrayCircularReferenceComparison(t *testing.T) {
112+
const SCRIPT = `
113+
var T = [1, 2, 3];
114+
T[42] = T; // Create circular reference
115+
try {
116+
var result = T == 5; // Comparison should not crash
117+
true;
118+
} catch (e) {
119+
false;
120+
}
121+
`
122+
testScript(SCRIPT, valueTrue, t)
123+
}

builtin_proxy_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,3 +1273,13 @@ func TestProxyEnumerableSymbols(t *testing.T) {
12731273

12741274
testScriptWithTestLib(SCRIPT, valueTrue, t)
12751275
}
1276+
1277+
func TestProxyUnicodeProps(t *testing.T) {
1278+
const SCRIPT = `
1279+
const proxy = new Proxy({}, {
1280+
ownKeys: () => ["â"]
1281+
});
1282+
compareArray(Reflect.ownKeys(proxy), ["â"]);
1283+
`
1284+
testScriptWithTestLib(SCRIPT, valueTrue, t)
1285+
}

builtin_string.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ func (r *Runtime) _stringPad(call FunctionCall, start bool) Value {
524524
return asciiString(sb.String())
525525
}
526526
var sb unicodeStringBuilder
527-
sb.ensureStarted(toIntStrict(maxLength))
527+
sb.Grow(toIntStrict(maxLength))
528528
if !start {
529529
sb.writeString(s)
530530
}

builtin_typedarrays.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,8 +1060,8 @@ func (r *Runtime) typedArrayProto_set(call FunctionCall) Value {
10601060
}
10611061
for i := 0; i < srcLen; i++ {
10621062
val := nilSafe(srcObj.self.getIdx(valueInt(i), nil))
1063-
if ta.isValidIntegerIndex(i) {
1064-
ta.typedArray.set(targetOffset+i, val)
1063+
if ta.isValidIntegerIndex(targetOffset + i) {
1064+
ta.typedArray.set(ta.offset+targetOffset+i, val)
10651065
}
10661066
}
10671067
}

builtin_typedarrays_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,3 +367,19 @@ func TestDataViewExportToBytes(t *testing.T) {
367367
t.Fatal("unexpected value", b)
368368
}
369369
}
370+
371+
func TestTypedArraySubarraySet(t *testing.T) {
372+
const SCRIPT = `
373+
const u = new Uint8Array(4)
374+
const s = u.subarray(1, 4);
375+
s.set([1,2,3], 0);
376+
if (s.length !== 3) {
377+
throw new Error("s.length=" + s.length + ", expected 3");
378+
}
379+
if (s[0] !== 1 || s[1] !== 2 || s[2] !== 3) {
380+
throw new Error("s=[" + s.join(",") + "], expected [1,2,3]");
381+
}
382+
`
383+
384+
testScript(SCRIPT, _undefined, t)
385+
}

destruct.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package sobek
22

33
import (
4-
"github.com/grafana/sobek/unistring"
54
"reflect"
5+
6+
"github.com/grafana/sobek/unistring"
67
)
78

89
type destructKeyedSource struct {
910
r *Runtime
1011
wrapped Value
11-
usedKeys map[Value]struct{}
12+
usedKeys propNameSet
1213
}
1314

1415
func newDestructKeyedSource(r *Runtime, wrapped Value) *destructKeyedSource {
@@ -30,10 +31,7 @@ func (d *destructKeyedSource) w() objectImpl {
3031
}
3132

3233
func (d *destructKeyedSource) recordKey(key Value) {
33-
if d.usedKeys == nil {
34-
d.usedKeys = make(map[Value]struct{})
35-
}
36-
d.usedKeys[key] = struct{}{}
34+
d.usedKeys.add(key)
3735
}
3836

3937
func (d *destructKeyedSource) sortLen() int {
@@ -202,7 +200,7 @@ func (i *destructKeyedSourceIter) next() (propIterItem, iterNextFunc) {
202200
return item, nil
203201
}
204202
i.wrapped = next
205-
if _, exists := i.d.usedKeys[item.name]; !exists {
203+
if !i.d.usedKeys.has(item.name) {
206204
return item, i.next
207205
}
208206
}
@@ -268,7 +266,7 @@ func (d *destructKeyedSource) stringKeys(all bool, accum []Value) []Value {
268266
func (d *destructKeyedSource) filterUsedKeys(keys []Value) []Value {
269267
k := 0
270268
for i, key := range keys {
271-
if _, exists := d.usedKeys[key]; exists {
269+
if d.usedKeys.has(key) {
272270
continue
273271
}
274272
if k != i {

object.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1816,3 +1816,43 @@ func (i *privateId) String() string {
18161816
func (i *privateId) string() unistring.String {
18171817
return privateIdString(i.name)
18181818
}
1819+
1820+
type propNameSet struct {
1821+
stringProps map[unistring.String]struct{}
1822+
symbolProps map[*Symbol]struct{}
1823+
}
1824+
1825+
func (s *propNameSet) add(prop Value) {
1826+
if sym, ok := prop.(*Symbol); ok {
1827+
if s.symbolProps == nil {
1828+
s.symbolProps = make(map[*Symbol]struct{})
1829+
}
1830+
s.symbolProps[sym] = struct{}{}
1831+
} else {
1832+
if s.stringProps == nil {
1833+
s.stringProps = make(map[unistring.String]struct{})
1834+
}
1835+
s.stringProps[prop.string()] = struct{}{}
1836+
}
1837+
}
1838+
1839+
func (s *propNameSet) has(prop Value) bool {
1840+
if sym, ok := prop.(*Symbol); ok {
1841+
_, exists := s.symbolProps[sym]
1842+
return exists
1843+
}
1844+
_, exists := s.stringProps[prop.string()]
1845+
return exists
1846+
}
1847+
1848+
func (s *propNameSet) delete(prop Value) {
1849+
if sym, ok := prop.(*Symbol); ok {
1850+
delete(s.symbolProps, sym)
1851+
} else {
1852+
delete(s.stringProps, prop.string())
1853+
}
1854+
}
1855+
1856+
func (s *propNameSet) size() int {
1857+
return len(s.stringProps) + len(s.symbolProps)
1858+
}

parser/parser_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,12 @@ func TestParser(t *testing.T) {
930930
`, nil)
931931
is(len(program.Body), 1)
932932

933+
test("function f() { if (true) { return class A {} } let A; if (true) { A = class A {} } }", nil)
934+
test("function f() { if (true) return class A {} }", nil)
935+
test("function f() { let A; if (true) A = class A {} }", nil)
936+
test("function f() { if (false) {} else return class A {} }", nil)
937+
test("function f() { let A; if (false) {} else A = class A {} }", nil)
938+
933939
{
934940
program := test(`(-2)**53`, nil)
935941
st := program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression)

parser/statement.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ func (self *_parser) parseArrowFunctionBody(async bool) (ast.ConciseBody, []*ast
337337
}
338338

339339
func (self *_parser) parseClass(declaration bool) *ast.ClassLiteral {
340-
if !self.scope.allowLet && self.token == token.CLASS {
340+
if declaration && !self.scope.allowLet && self.token == token.CLASS {
341341
self.errorUnexpectedToken(token.CLASS)
342342
}
343343

0 commit comments

Comments
 (0)