type UnserializedScalar = string | number | boolean | null type UnserializedObject = { [key: string]: UnserializedValue } type UnserializedValue = UnserializedScalar | UnserializedObject | UnserializedValue[]
type ParsedResult = [value: UnserializedValue, offset: number] type CacheEntry = [value: UnserializedValue, offset?: number] type CacheFn = (<T extends CacheEntry>(value: T) => T) & { get: (index: number) => UnserializedValue } type ErrorMode = 'throw' | 'log' | 'silent' type UnserializeInput = string | null | undefined
function initCache(): CacheFn { const store: UnserializedValue[] = [] const cacheBase = function cache<T extends CacheEntry>(value: T): T { store.push(value[0]) return value }
const cache: CacheFn = Object.assign(cacheBase, { get: (index: number): UnserializedValue => { if (index >= store.length) { throw new RangeError(`Can't resolve reference ${index + 1}`) }
const cachedValue = store[index] if (typeof cachedValue === 'undefined') { throw new RangeError(`Can't resolve reference ${index + 1}`) }
return cachedValue }, })
return cache }
function expectType(str: string, cache: CacheFn): ParsedResult { const types = /^(?:N(?=;)|[bidsSaOCrR](?=:)|[^:]+(?=:))/g const type = (types.exec(str) || [])[0]
if (!type) { throw new 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 new SyntaxError(`Invalid or unsupported data type: ${type}`) } }
function expectBool(str: string): [boolean, number] { const reBool = /^b:([01]);/ const [match, boolMatch] = reBool.exec(str) || []
if (!match || !boolMatch) { throw new SyntaxError('Invalid bool value, expected 0 or 1') }
return [boolMatch === '1', match.length] }
function expectInt(str: string): [number, number] { const reInt = /^i:([+-]?\d+);/ const [match, intMatch] = reInt.exec(str) || []
if (!match || !intMatch) { throw new SyntaxError('Expected an integer value') }
return [parseInt(intMatch, 10), match.length] }
function expectFloat(str: string): [number, number] { const reFloat = /^d:(NAN|-?INF|(?:\d+\.\d*|\d*\.\d+|\d+)(?:[eE][+-]\d+)?);/ const [match, floatMatch] = reFloat.exec(str) || []
if (!match || !floatMatch) { throw new SyntaxError('Expected a float value') }
let floatValue = 0
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: string, len: number, escapedString = false): [string, number, number] { 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++
c += 2 }
c++
bytes += isHighSurrogate || (isLowSurrogate && wasHighSurrogate) ? 2 : code > 0x7ff ? 3 : code > 0x7f ? 2 : 1
bytes += wasHighSurrogate && !isLowSurrogate ? 1 : 0
out += chr wasHighSurrogate = isHighSurrogate }
return [out, bytes, escapedChars] }
function expectString(str: string): [string, number] { const reStrLength = /^s:(\d+):"/g const [match, byteLenMatch] = reStrLength.exec(str) || []
if (!match || !byteLenMatch) { throw new 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 new SyntaxError(`Expected string of ${len} bytes, but got ${bytes}`) }
str = str.substr(strMatch.length)
if (!str.startsWith('";')) { throw new SyntaxError('Expected ";') }
return [strMatch, match.length + strMatch.length + 2] }
function expectEscapedString(str: string): [string, number] { const reStrLength = /^S:(\d+):"/g const [match, strLenMatch] = reStrLength.exec(str) || []
if (!match || !strLenMatch) { throw new 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 new SyntaxError(`Expected escaped string of ${len} bytes, but got ${bytes}`) }
str = str.substr(strMatch.length + escapedChars * 2)
if (!str.startsWith('";')) { throw new SyntaxError('Expected ";') }
return [strMatch, match.length + strMatch.length + 2] }
function expectKeyOrIndex(str: string): [string | number, number] { try { return expectString(str) } catch (_err) {}
try { return expectEscapedString(str) } catch (_err) {}
try { return expectInt(str) } catch (_err) { throw new SyntaxError('Expected key or index') } }
function expectObject(str: string, cache: CacheFn): ParsedResult { const reObjectLiteral = /^O:(\d+):"([^"]+)":(\d+):\{/ const [objectLiteralBeginMatch , , className, propCountMatch] = reObjectLiteral.exec(str) || []
if (!objectLiteralBeginMatch || !propCountMatch) { throw new SyntaxError('Invalid input') }
if (className !== 'stdClass') { throw new SyntaxError(`Unsupported object type: ${className}`) }
let totalOffset = objectLiteralBeginMatch.length
const propCount = parseInt(propCountMatch, 10) const obj: UnserializedObject = {} 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[String(prop[0])] = value[0] }
if (str.charAt(0) !== '}') { throw new SyntaxError('Expected }') }
return [obj, totalOffset + 1] }
function expectClass(_str: string, _cache: CacheFn): ParsedResult { throw new Error('Not yet implemented') }
function expectReference(str: string, cache: CacheFn): ParsedResult { const reRef = /^[rR]:([1-9]\d*);/ const [match, refIndex] = reRef.exec(str) || []
if (!match || !refIndex) { throw new SyntaxError('Expected reference value') }
return [cache.get(parseInt(refIndex, 10) - 1), match.length] }
function expectArray(str: string, cache: CacheFn): ParsedResult { const reArrayLength = /^a:(\d+):{/ const [arrayLiteralBeginMatch, arrayLengthMatch] = reArrayLength.exec(str) || []
if (!arrayLiteralBeginMatch || !arrayLengthMatch) { throw new SyntaxError('Expected array length annotation') }
str = str.substr(arrayLiteralBeginMatch.length)
const array = expectArrayItems(str, parseInt(arrayLengthMatch, 10), cache)
if (str.charAt(array[1]) !== '}') { throw new SyntaxError('Expected }') }
return [array[0], arrayLiteralBeginMatch.length + array[1] + 1] }
function expectArrayItems( str: string, expectedItems = 0, cache: CacheFn, ): [UnserializedObject | UnserializedValue[], number] { let key: [string | number, number] let item: ParsedResult let totalOffset = 0 let hasContinousIndexes = true let lastIndex = -1 const items: UnserializedObject = {} cache([items])
for (let i = 0; i < expectedItems; i++) { key = expectKeyOrIndex(str)
hasContinousIndexes = hasContinousIndexes && typeof key[0] === 'number' && key[0] === lastIndex + 1 lastIndex = typeof key[0] === 'number' ? key[0] : lastIndex
str = str.substr(key[1]) totalOffset += key[1]
item = expectType(str, cache) str = str.substr(item[1]) totalOffset += item[1]
items[String(key[0])] = item[0] }
if (hasContinousIndexes) { return [Object.values(items), totalOffset] }
return [items, totalOffset] }
export function unserialize(str: UnserializeInput, errorMode: ErrorMode = 'log'): UnserializedValue | false {
try { if (typeof str !== 'string') { return false }
return expectType(str, initCache())[0] } catch (err) { if (errorMode === 'throw') { throw err } else if (errorMode === 'log') { console.error(err) } return false } }
|