Skip to content

Commit 3b6684d

Browse files
authored
Failsafe for anonymous functions in encode
1 parent b22ff51 commit 3b6684d

File tree

6 files changed

+84
-53
lines changed

6 files changed

+84
-53
lines changed

src/CryptoUtils.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Helper function that turns a string into a unique 53-bit hash.
3+
* @ref https://stackoverflow.com/a/52171480/6456163
4+
* @param {string} str
5+
* @param {number} seed
6+
* @returns {number}
7+
*/
8+
export const cyrb53 = (str, seed = 0) => {
9+
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
10+
for (let i = 0, ch; i < str.length; i++) {
11+
ch = str.charCodeAt(i);
12+
h1 = Math.imul(h1 ^ ch, 2654435761);
13+
h2 = Math.imul(h2 ^ ch, 1597334677);
14+
}
15+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
16+
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
17+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
18+
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
19+
20+
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
21+
};

src/ParseOp.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export class SetOp extends Op {
7878
}
7979

8080
toJSON(offline?: boolean) {
81-
return encode(this._value, false, true, undefined, offline);
81+
return encode(this._value, false, true, undefined, offline, 0);
8282
}
8383
}
8484

src/__tests__/ParseObject-test.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3878,13 +3878,4 @@ describe('ParseObject pin', () => {
38783878
'Traversing object failed due to high number of recursive calls, likely caused by circular reference within object.'
38793879
);
38803880
});
3881-
3882-
it('throws error for infinite recursion', () => {
3883-
const circularObject = {};
3884-
circularObject.circularReference = circularObject;
3885-
3886-
expect(() => {
3887-
encode(circularObject, false, false, [], false);
3888-
}).toThrowError('Encoding object failed due to high number of recursive calls, likely caused by circular reference within object.');
3889-
});
38903881
});

src/__tests__/encode-test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,13 @@ describe('encode', () => {
183183
str: 'abc',
184184
});
185185
});
186+
187+
it('handles circular references', () => {
188+
const circularObject = {};
189+
circularObject.circularReference = circularObject;
190+
191+
expect(() => {
192+
encode(circularObject, false, false, [], false);
193+
}).not.toThrow();
194+
});
186195
});

src/encode.js

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import ParsePolygon from './ParsePolygon';
99
import ParseObject from './ParseObject';
1010
import { Op } from './ParseOp';
1111
import ParseRelation from './ParseRelation';
12+
import { cyrb53 } from './CryptoUtils';
1213

1314
const MAX_RECURSIVE_CALLS = 999;
1415

@@ -18,18 +19,15 @@ function encode(
1819
forcePointers: boolean,
1920
seen: Array<mixed>,
2021
offline: boolean,
21-
counter: number = 0
22+
counter: number,
23+
initialValue: mixed
2224
): any {
2325
counter++;
2426

2527
if (counter > MAX_RECURSIVE_CALLS) {
2628
const message = 'Encoding object failed due to high number of recursive calls, likely caused by circular reference within object.';
2729
console.error(message);
28-
console.error('Value causing potential infinite recursion:', value);
29-
console.error('Disallow objects:', disallowObjects);
30-
console.error('Force pointers:', forcePointers);
31-
console.error('Seen:', seen);
32-
console.error('Offline:', offline);
30+
console.error('Value causing potential infinite recursion:', initialValue);
3331

3432
throw new Error(message);
3533
}
@@ -38,73 +36,90 @@ function encode(
3836
if (disallowObjects) {
3937
throw new Error('Parse Objects not allowed here');
4038
}
41-
const seenEntry = value.id ? value.className + ':' + value.id : value;
39+
const entryIdentifier = value.id ? value.className + ':' + value.id : value;
4240
if (
4341
forcePointers ||
44-
!seen ||
45-
seen.indexOf(seenEntry) > -1 ||
42+
seen.includes(entryIdentifier) ||
4643
value.dirty() ||
47-
Object.keys(value._getServerData()).length < 1
44+
Object.keys(value._getServerData()).length === 0
4845
) {
4946
if (offline && value._getId().startsWith('local')) {
5047
return value.toOfflinePointer();
5148
}
5249
return value.toPointer();
5350
}
54-
seen = seen.concat(seenEntry);
51+
seen.push(entryIdentifier);
5552
return value._toFullJSON(seen, offline);
56-
}
57-
if (
53+
} else if (
5854
value instanceof Op ||
5955
value instanceof ParseACL ||
6056
value instanceof ParseGeoPoint ||
6157
value instanceof ParsePolygon ||
6258
value instanceof ParseRelation
6359
) {
6460
return value.toJSON();
65-
}
66-
if (value instanceof ParseFile) {
61+
} else if (value instanceof ParseFile) {
6762
if (!value.url()) {
6863
throw new Error('Tried to encode an unsaved file.');
6964
}
7065
return value.toJSON();
71-
}
72-
if (Object.prototype.toString.call(value) === '[object Date]') {
66+
} else if (Object.prototype.toString.call(value) === '[object Date]') {
7367
if (isNaN(value)) {
7468
throw new Error('Tried to encode an invalid date.');
7569
}
7670
return { __type: 'Date', iso: (value: any).toJSON() };
77-
}
78-
if (
71+
} else if (
7972
Object.prototype.toString.call(value) === '[object RegExp]' &&
8073
typeof value.source === 'string'
8174
) {
8275
return value.source;
83-
}
84-
85-
if (Array.isArray(value)) {
86-
return value.map(v => {
87-
return encode(v, disallowObjects, forcePointers, seen, offline, counter);
88-
});
89-
}
90-
91-
if (value && typeof value === 'object') {
76+
} else if (Array.isArray(value)) {
77+
return value.map(v => encode(v, disallowObjects, forcePointers, seen, offline, counter, initialValue));
78+
} else if (value && typeof value === 'object') {
9279
const output = {};
9380
for (const k in value) {
94-
output[k] = encode(value[k], disallowObjects, forcePointers, seen, offline, counter);
81+
try {
82+
// Attempts to get the name of the object's constructor
83+
// Ref: https://stackoverflow.com/a/332429/6456163
84+
const name = value[k].name || value[k].constructor.name;
85+
if (name && name != "undefined") {
86+
if (seen.includes(name)) {
87+
output[k] = value[k];
88+
continue;
89+
} else {
90+
seen.push(name);
91+
}
92+
}
93+
} catch (e) {
94+
// Support anonymous functions by hashing the function body,
95+
// preventing infinite recursion in the case of circular references
96+
if (value[k] instanceof Function) {
97+
const funcString = value[k].toString();
98+
if (seen.includes(funcString)) {
99+
output[k] = value[k];
100+
continue;
101+
} else {
102+
const hash = cyrb53(funcString);
103+
seen.push(hash);
104+
}
105+
}
106+
}
107+
output[k] = encode(value[k], disallowObjects, forcePointers, seen, offline, counter, initialValue);
95108
}
96109
return output;
110+
} else {
111+
return value;
97112
}
98-
99-
return value;
100113
}
101114

102115
export default function (
103116
value: mixed,
104117
disallowObjects?: boolean,
105118
forcePointers?: boolean,
106119
seen?: Array<mixed>,
107-
offline?: boolean
120+
offline?: boolean,
121+
counter?: number,
122+
initialValue?: mixed
108123
): any {
109-
return encode(value, !!disallowObjects, !!forcePointers, seen || [], offline, 0);
124+
return encode(value, !!disallowObjects, !!forcePointers, seen || [], !!offline, counter || 0, initialValue || value);
110125
}

src/unsavedChildren.js

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ function traverse(
5858
if (counter > MAX_RECURSIVE_CALLS) {
5959
const message = 'Traversing object failed due to high number of recursive calls, likely caused by circular reference within object.';
6060
console.error(message);
61-
console.error('Object causing potential infinite recursion:', obj);
6261
console.error('Encountered objects:', encountered);
62+
console.error('Object causing potential infinite recursion:', obj);
6363

6464
throw new Error(message);
6565
}
@@ -89,16 +89,11 @@ function traverse(
8989
if (obj instanceof ParseRelation) {
9090
return;
9191
}
92-
if (Array.isArray(obj)) {
93-
obj.forEach(el => {
94-
if (typeof el === 'object') {
95-
traverse(el, encountered, shouldThrow, allowDeepUnsaved, counter);
92+
if (Array.isArray(obj) || typeof obj === 'object') {
93+
for (const k in obj) {
94+
if (typeof obj[k] === 'object') {
95+
traverse(obj[k], encountered, shouldThrow, allowDeepUnsaved, counter);
9696
}
97-
});
98-
}
99-
for (const k in obj) {
100-
if (typeof obj[k] === 'object') {
101-
traverse(obj[k], encountered, shouldThrow, allowDeepUnsaved, counter);
10297
}
10398
}
10499
}

0 commit comments

Comments
 (0)