From 8318911c08e6ca65d5757be388a2dad9f226972e Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Fri, 6 Dec 2024 14:39:19 -0800 Subject: [PATCH] fix: Avoid stack overflow when stringifying objects (#7721) Add special case for arrays, for compatibility with frameworks or polyfills that add properties to Array or Array instances. Add special case for functions, which always contain circular references and are unexpected in this context. These seem to appear because of the frameworks/polyfills mentioned above. Move everything to ObjectUtils, since this is extremely generic. Closes #7435 --- lib/util/object_utils.js | 83 ++++++++++++++++++++++++++++++++++++++++ lib/util/stream_utils.js | 36 +---------------- 2 files changed, 84 insertions(+), 35 deletions(-) diff --git a/lib/util/object_utils.js b/lib/util/object_utils.js index c62eb73f98..7d427eadb2 100644 --- a/lib/util/object_utils.js +++ b/lib/util/object_utils.js @@ -89,4 +89,87 @@ shaka.util.ObjectUtils = class { } return clone; } + + + /** + * Constructs a string out of a value, similar to the JSON.stringify method. + * Unlike that method, this guarantees that the order of the keys in an + * object is alphabetical, so it can be used as a way to reliably compare two + * objects. + * + * @param {?} value + * @return {string} + */ + static alphabeticalKeyOrderStringify(value) { + if (Array.isArray(value)) { + return shaka.util.ObjectUtils.arrayStringify_(value); + } else if (typeof value == 'function') { + // For safety, skip functions. For function x, + // x.prototype.constructor.prototype === x.prototype, so all functions + // contain circular references if treated like Objects. + return ''; + } else if (value instanceof Object) { + return shaka.util.ObjectUtils.objectStringify_(value); + } else { + return JSON.stringify(value); + } + } + + + /** + * Helper for alphabeticalKeyOrderStringify for objects. + * + * @param {!Object} obj + * @return {string} + * @private + */ + static objectStringify_(obj) { + // NOTE: This excludes prototype chain keys. For now, this is intended for + // anonymous objects only, so we don't care. If that changes, go back to a + // for-in loop. + const keys = Object.keys(obj); + // Alphabetically sort the keys, so they will be in a reliable order. + keys.sort(); + + const terms = []; + for (const key of keys) { + const escapedKey = JSON.stringify(key); + const value = obj[key]; + if (value !== undefined) { + const escapedValue = + shaka.util.ObjectUtils.alphabeticalKeyOrderStringify(value); + if (escapedValue) { + terms.push(escapedKey + ':' + escapedValue); + } + } + } + return '{' + terms.join(',') + '}'; + } + + + /** + * Helper for alphabeticalKeyOrderStringify for arrays. + * + * This could itself be JSON.stringify, except we want objects within the + * array to go through our own stringifiers. + * + * @param {!Array} arr + * @return {string} + * @private + */ + static arrayStringify_(arr) { + const terms = []; + for (let index = 0; index < arr.length; index++) { + const escapedKey = index.toString(); + const value = arr[index]; + if (value !== undefined) { + const escapedValue = + shaka.util.ObjectUtils.alphabeticalKeyOrderStringify(value); + if (escapedValue) { + terms.push(escapedKey + ':' + escapedValue); + } + } + } + return '[' + terms.join(',') + ']'; + } }; diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js index 476b6007ae..f31657112b 100644 --- a/lib/util/stream_utils.js +++ b/lib/util/stream_utils.js @@ -545,40 +545,6 @@ shaka.util.StreamUtils = class { } - /** - * Constructs a string out of an object, similar to the JSON.stringify method. - * Unlike that method, this guarantees that the order of the keys is - * alphabetical, so it can be used as a way to reliably compare two objects. - * - * @param {!Object} obj - * @return {string} - * @private - */ - static alphabeticalKeyOrderStringify_(obj) { - const keys = []; - for (const key in obj) { - keys.push(key); - } - // Alphabetically sort the keys, so they will be in a reliable order. - keys.sort(); - - const terms = []; - for (const key of keys) { - const escapedKey = JSON.stringify(key); - const value = obj[key]; - if (value instanceof Object) { - const stringifiedValue = - shaka.util.StreamUtils.alphabeticalKeyOrderStringify_(value); - terms.push(escapedKey + ':' + stringifiedValue); - } else { - const escapedValue = JSON.stringify(value); - terms.push(escapedKey + ':' + escapedValue); - } - } - return '{' + terms.join(',') + '}'; - } - - /** * Queries mediaCapabilities for the decoding info for that decoding config, * and assigns it to the given variant. @@ -614,7 +580,7 @@ shaka.util.StreamUtils = class { const promises = []; for (const decodingConfig of decodingConfigs) { const cacheKey = - StreamUtils.alphabeticalKeyOrderStringify_(decodingConfig); + shaka.util.ObjectUtils.alphabeticalKeyOrderStringify(decodingConfig); const cache = StreamUtils.decodingConfigCache_; if (cache[cacheKey]) {