From bf4093f407aa308ec854032e7f562f524960e423 Mon Sep 17 00:00:00 2001 From: Izzy <360964+izzy@users.noreply.github.com> Date: Sat, 23 Oct 2021 22:59:19 +0200 Subject: [PATCH 1/2] Updated unserialize This should fix #3 and also work in more cases where before it would fail. This also includes error handling for unserialize ops. --- js/D3.js | 485 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 357 insertions(+), 128 deletions(-) diff --git a/js/D3.js b/js/D3.js index 2854c1c..d7ef970 100644 --- a/js/D3.js +++ b/js/D3.js @@ -996,136 +996,365 @@ var D3 = return hash_map; }, unserialize: function (data) { - var that = this; - var utf8Overhead = function(chr) { - var code = chr.charCodeAt(0); - if (code < 0x0080) { - return 0; - } - if (code < 0x0800) { - return 1; - } - return 2; - }; + data = data + ''; + + function initCache() { + const store = [] + // cache only first element, second is length to jump ahead for the parser + const cache = function cache(value) { + store.push(value[0]) + return value + } + cache.get = (index) => { + if (index >= store.length) { + throw RangeError(`Can't resolve reference ${index + 1}`) + } - var error = function (type, msg, filename, line){throw new that.window[type](msg, filename, line);}; - var read_until = function (data, offset, stopchr){ - var buf = []; - var chr = data.slice(offset, offset + 1); - var i = 2; - while (chr != stopchr) { - if ((i+offset) > data.length) { - error('Error', 'Invalid'); - } - buf.push(chr); - chr = data.slice(offset + (i - 1),offset + i); - i += 1; - } - return [buf.length, buf.join('')]; - }; - var read_chrs = function (data, offset, length){ - var buf; - - buf = []; - for (var i = 0;i < length;i++){ - var chr = data.slice(offset + (i - 1),offset + i); - buf.push(chr); - length -= utf8Overhead(chr); - } - return [buf.length, buf.join('')]; - }; - var _unserialize = function (data, offset){ - var readdata; - var readData; - var chrs = 0; - var ccount; - var stringlength; - var keyandchrs; - var keys; - - if (!offset) {offset = 0;} - var dtype = (data.slice(offset, offset + 1)).toLowerCase(); - - var dataoffset = offset + 2; - var typeconvert = function(x) {return x;}; - - switch (dtype){ - case 'i': - typeconvert = function (x) {return parseInt(x, 10);}; - readData = read_until(data, dataoffset, ';'); - chrs = readData[0]; - readdata = readData[1]; - dataoffset += chrs + 1; - break; - case 'b': - typeconvert = function (x) {return parseInt(x, 10) !== 0;}; - readData = read_until(data, dataoffset, ';'); - chrs = readData[0]; - readdata = readData[1]; - dataoffset += chrs + 1; - break; - case 'd': - typeconvert = function (x) {return parseFloat(x);}; - readData = read_until(data, dataoffset, ';'); - chrs = readData[0]; - readdata = readData[1]; - dataoffset += chrs + 1; - break; - case 'n': - readdata = null; - break; - case 's': - ccount = read_until(data, dataoffset, ':'); - chrs = ccount[0]; - stringlength = ccount[1]; - dataoffset += chrs + 2; - - readData = read_chrs(data, dataoffset+1, parseInt(stringlength, 10)); - chrs = readData[0]; - readdata = readData[1]; - dataoffset += chrs + 2; - if (chrs != parseInt(stringlength, 10) && chrs != readdata.length){ - error('SyntaxError', 'String length mismatch'); - } - - // Length was calculated on an utf-8 encoded string - // so wait with decoding - readdata = D3.utf8_decode(readdata); - break; - case 'a': - readdata = {}; - - keyandchrs = read_until(data, dataoffset, ':'); - chrs = keyandchrs[0]; - keys = keyandchrs[1]; - dataoffset += chrs + 2; - - for (var i = 0; i < parseInt(keys, 10); i++){ - var kprops = _unserialize(data, dataoffset); - var kchrs = kprops[1]; - var key = kprops[2]; - dataoffset += kchrs; - - var vprops = _unserialize(data, dataoffset); - var vchrs = vprops[1]; - var value = vprops[2]; - dataoffset += vchrs; - - readdata[key] = value; - } - - dataoffset += 1; - break; - default: - error('SyntaxError', 'Unknown / Unhandled data type(s): ' + dtype); - break; - } - return [dtype, dataoffset - offset, typeconvert(readdata)]; - }; - - return D3.var_dump(_unserialize((data+''), 0)[2]); - }, + return store[index] + } + + return cache + } + + function expectType(str, cache) { + const types = /^(?:N(?=;)|[bidsSaOCrR](?=:)|[^:]+(?=:))/g + const type = (types.exec(str) || [])[0] + + if (!type) { + throw SyntaxError('Invalid input: ' + str) + } + + switch (type) { + case 'N': + return cache([null, 2]) + case 'b': + return cache(expectBool(str)) + case 'i': + return cache(expectInt(str)) + case 'd': + return cache(expectFloat(str)) + case 's': + return cache(expectString(str)) + case 'S': + return cache(expectEscapedString(str)) + case 'a': + return expectArray(str, cache) + case 'O': + return expectObject(str, cache) + case 'C': + return expectClass(str, cache) + case 'r': + case 'R': + return expectReference(str, cache) + default: + throw SyntaxError(`Invalid or unsupported data type: ${type}`) + } + } + + function expectBool(str) { + const reBool = /^b:([01]);/ + const [match, boolMatch] = reBool.exec(str) || [] + + if (!boolMatch) { + throw SyntaxError('Invalid bool value, expected 0 or 1') + } + + return [boolMatch === '1', match.length] + } + + function expectInt(str) { + const reInt = /^i:([+-]?\d+);/ + const [match, intMatch] = reInt.exec(str) || [] + + if (!intMatch) { + throw SyntaxError('Expected an integer value') + } + + return [parseInt(intMatch, 10), match.length] + } + + function expectFloat(str) { + const reFloat = /^d:(NAN|-?INF|(?:\d+\.\d*|\d*\.\d+|\d+)(?:[eE][+-]\d+)?);/ + const [match, floatMatch] = reFloat.exec(str) || [] + + if (!floatMatch) { + throw SyntaxError('Expected a float value') + } + + let floatValue + + switch (floatMatch) { + case 'NAN': + floatValue = Number.NaN + break + case '-INF': + floatValue = Number.NEGATIVE_INFINITY + break + case 'INF': + floatValue = Number.POSITIVE_INFINITY + break + default: + floatValue = parseFloat(floatMatch) + break + } + + return [floatValue, match.length] + } + + function readBytes(str, len, escapedString = false) { + let bytes = 0 + let out = '' + let c = 0 + const strLen = str.length + let wasHighSurrogate = false + let escapedChars = 0 + + while (bytes < len && c < strLen) { + let chr = str.charAt(c) + const code = chr.charCodeAt(0) + const isHighSurrogate = code >= 0xd800 && code <= 0xdbff + const isLowSurrogate = code >= 0xdc00 && code <= 0xdfff + + if (escapedString && chr === '\\') { + chr = String.fromCharCode(parseInt(str.substr(c + 1, 2), 16)) + escapedChars++ + + // each escaped sequence is 3 characters. Go 2 chars ahead. + // third character will be jumped over a few lines later + c += 2 + } + + c++ + + bytes += isHighSurrogate || (isLowSurrogate && wasHighSurrogate) + // if high surrogate, count 2 bytes, as expectation is to be followed by low surrogate + // if low surrogate preceded by high surrogate, add 2 bytes + ? + 2 : + code > 0x7ff + // otherwise low surrogate falls into this part + ? + 3 : + code > 0x7f ? + 2 : + 1 + + // if high surrogate is not followed by low surrogate, add 1 more byte + bytes += wasHighSurrogate && !isLowSurrogate ? 1 : 0; + + out += chr; + wasHighSurrogate = isHighSurrogate; + } + + return [out, bytes, escapedChars]; + } + + function expectString(str) { + // PHP strings consist of one-byte characters. + // JS uses 2 bytes with possible surrogate pairs. + // Serialized length of 2 is still 1 JS string character + const reStrLength = /^s:(\d+):"/g; // also match the opening " char + const [match, byteLenMatch] = reStrLength.exec(str) || []; + + if (!match) { + throw SyntaxError('Expected a string value'); + } + + const len = parseInt(byteLenMatch, 10); + + str = str.substr(match.length); + + const [strMatch, bytes] = readBytes(str, len); + + if (bytes !== len) { + throw SyntaxError(`Expected string of ${len} bytes, but got ${bytes}`); + } + + str = str.substr(strMatch.length); + + // strict parsing, match closing "; chars + if (!str.startsWith('";')) { + throw SyntaxError('Expected ";'); + } + + return [strMatch, match.length + strMatch.length + 2]; // skip last "; + } + + function expectEscapedString(str) { + const reStrLength = /^S:(\d+):"/g; // also match the opening " char + const [match, strLenMatch] = reStrLength.exec(str) || []; + + if (!match) { + throw SyntaxError('Expected an escaped string value'); + } + + const len = parseInt(strLenMatch, 10); + + str = str.substr(match.length); + + const [strMatch, bytes, escapedChars] = readBytes(str, len, true); + + if (bytes !== len) { + throw SyntaxError(`Expected escaped string of ${len} bytes, but got ${bytes}`); + } + + str = str.substr(strMatch.length + escapedChars * 2); + + // strict parsing, match closing "; chars + if (!str.startsWith('";')) { + throw SyntaxError('Expected ";'); + } + + return [strMatch, match.length + strMatch.length + 2]; // skip last "; + } + + function expectKeyOrIndex(str) { + try { + return expectString(str); + } catch (err) {} + + try { + return expectEscapedString(str); + } catch (err) {} + + try { + return expectInt(str); + } catch (err) { + throw SyntaxError('Expected key or index'); + } + } + + function expectObject(str, cache) { + // O::"class name"::{} + // O:8:"stdClass":2:{s:3:"foo";s:3:"bar";s:3:"bar";s:3:"baz";} + const reObjectLiteral = /^O:(\d+):"([^"]+)":(\d+):\{/; + const [objectLiteralBeginMatch, /* classNameLengthMatch */ , className, propCountMatch] = reObjectLiteral.exec(str) || []; + + if (!objectLiteralBeginMatch) { + throw SyntaxError('Invalid input'); + } + + if (className !== 'stdClass') { + throw SyntaxError(`Unsupported object type: ${className}`); + } + + let totalOffset = objectLiteralBeginMatch.length; + + const propCount = parseInt(propCountMatch, 10); + const obj = {}; + cache([obj]); + + str = str.substr(totalOffset); + + for (let i = 0; i < propCount; i++) { + const prop = expectKeyOrIndex(str); + str = str.substr(prop[1]); + totalOffset += prop[1]; + + const value = expectType(str, cache); + str = str.substr(value[1]); + totalOffset += value[1]; + + obj[prop[0]] = value[0]; + } + + // strict parsing, expect } after object literal + if (str.charAt(0) !== '}') { + throw SyntaxError('Expected }'); + } + + return [obj, totalOffset + 1]; // skip final } + } + + function expectClass(str, cache) { + // can't be well supported, because requires calling eval (or similar) + // in order to call serialized constructor name + // which is unsafe + // or assume that constructor is defined in global scope + // but this is too much limiting + throw Error('Not yet implemented'); + } + + function expectReference(str, cache) { + const reRef = /^[rR]:([1-9]\d*);/; + const [match, refIndex] = reRef.exec(str) || []; + + if (!match) { + throw SyntaxError('Expected reference value'); + } + + return [cache.get(parseInt(refIndex, 10) - 1), match.length]; + } + + function expectArray(str, cache) { + const reArrayLength = /^a:(\d+):{/; + const [arrayLiteralBeginMatch, arrayLengthMatch] = reArrayLength.exec(str) || []; + + if (!arrayLengthMatch) { + throw SyntaxError('Expected array length annotation'); + } + + str = str.substr(arrayLiteralBeginMatch.length); + + const array = expectArrayItems(str, parseInt(arrayLengthMatch, 10), cache); + + // strict parsing, expect closing } brace after array literal + if (str.charAt(array[1]) !== '}') { + throw SyntaxError('Expected }'); + } + + return [array[0], arrayLiteralBeginMatch.length + array[1] + 1]; // jump over } + } + + function expectArrayItems(str, expectedItems = 0, cache) { + let key; + let hasStringKeys = false; + let item; + let totalOffset = 0; + let items = []; + cache([items]); + + for (let i = 0; i < expectedItems; i++) { + key = expectKeyOrIndex(str); + + // this is for backward compatibility with previous implementation + if (!hasStringKeys) { + hasStringKeys = (typeof key[0] === 'string'); + } + + str = str.substr(key[1]); + totalOffset += key[1]; + + // references are resolved immediately, so if duplicate key overwrites previous array index + // the old value is anyway resolved + // fixme: but next time the same reference should point to the new value + item = expectType(str, cache); + str = str.substr(item[1]); + totalOffset += item[1]; + + items[key[0]] = item[0]; + } + + // this is for backward compatibility with previous implementation + if (hasStringKeys) { + items = Object.assign({}, items); + } + + return [items, totalOffset]; + } + + try { + if (typeof data !== 'string') { + return false; + } + + return D3.var_dump(expectType(data, initCache())[0]); + } catch (err) { + console.error(err); + return err; + } + }, utf8_encode: function ( argString ) { var string = (argString+''); // .replace(/\r\n/g, "\n").replace(/\r/g, "\n"); From ee89964e8ea412a1207a6e697e6f095316409df0 Mon Sep 17 00:00:00 2001 From: Izzy <360964+izzy@users.noreply.github.com> Date: Sat, 23 Oct 2021 23:00:29 +0200 Subject: [PATCH 2/2] Updated changelog and version --- CHANGELOG.md | 2 ++ manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 178d23f..f9acf24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ CHANGELOG ============================= +* 4.8.0 + * Upgraded unserialize to newer version * 4.7.0 * Added Bengali translation * Added Bulgarian translation diff --git a/manifest.json b/manifest.json index 1fecde9..15c1fdc 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "__MSG_extName__", - "version": "4.7.0", + "version": "4.8.0", "description": "__MSG_extDescription__", "options_ui": { "page": "html/menu_ui.html",