// Yet another PHP-like serialize & unserialize & serializeSession & unserializeSession // Version 2021-04-09 // (c) Vitaliy Filippov, 2021+ exports.unserialize = unserialize; exports.unserializeSession = unserializeSession; exports.serialize = serialize; exports.serializeSession = serializeSession; const bareProto = {}.__proto__; /** * Serialize data in PHP's serialize() format * * @param any data * @param object typeNames * @return serialized data */ function serialize(data, typeNames) { if (typeof data === 'number') { if (data === (data|0)) return 'i:'+data+';'; else if (isNaN(data)) return 'd:NAN;'; else if (data == Infinity) return 'd:INF;'; else if (data == -Infinity) return 'd:-INF;'; else return 'd:'+data+';'; } else if (typeof data === 'boolean') { return 'b:'+(data ? 1 : 0)+';'; } else if (data == null) // or undefined { return 'N;'; } else if (typeof data === 'string') { return 's:'+utf8length(data)+':"'+data+'";'; } else if (data instanceof Array) { let s = 'a:'+data.length+':{'; data.forEach((d, i) => s += serialize(i, typeNames)+serialize(d, typeNames)); s += '}'; return s; } else if (data instanceof Object) { const keys = Object.keys(data); let s; if (data.__proto__ !== bareProto) { const type = typeNames && typeNames[data.__proto__.name] || data.__proto__.name; s = 'o:'+utf8length(type)+'"'+type+'":'; } else { s = 'a:'; } s += keys.length+':{'; keys.forEach(k => s += serialize(k, typeNames)+serialize(data[k], typeNames)); s += '}'; return s; } // Unsupported type throw new Error('Attempt to serialize an unsupported type'); } /** * Serialize data in PHP's session format (like session_encode()) * * @param object data * @return serialized session */ function serializeSession(data) { return Object.keys(data).filter(k => k.indexOf('|') < 0).map(k => k+'|'+serialize(data[k])).join(''); } /** * Unserialize data taken from PHP's serialize() output * * @param string serialized data * @return unserialized data * @throws */ function unserialize(data) { const u = new Unserializer(); return u.unserialize(data); } /** * Parse PHP-serialized session data * * @param string serialized session * @return unserialized data * @throws */ function unserializeSession(data) { const u = new Unserializer(); u.data = data; u.offset = 0; const res = {}; while (u.offset < data.length) { const pos = data.indexOf('|', u.offset); if (pos < 0) break; const key = data.substr(u.offset, pos-u.offset); u.offset = pos+1; res[key] = u.unserializeAt(); } return res; } function utf8charSize(code) { if (code < 0x0080) return 1; if (code < 0x0800) return 2; if (code < 0x10000) return 3; return 4; } function utf8length(str) { let l = 0; for (let i = 0; i < str.length; i++) l += utf8charSize(str.charCodeAt(i)); return l; } class Unserializer { readUntil(stopchr) { let pos = this.data.indexOf(stopchr, this.offset); if (pos < 0) throw new Error(stopchr+' expected after '+this.offset); let res = this.data.substr(this.offset, pos-this.offset); this.offset = pos+1; return res; } readChars(length) { let pos = this.offset; while (length > 0) { length -= utf8charSize(this.data.charCodeAt(pos)); pos++; } let res = this.data.substr(this.offset, pos-this.offset); this.offset = pos; return res; } readStr() { const bytelength = this.readUntil(':'); this.offset++; // " const str = this.readChars(parseInt(bytelength, 10)); this.offset += 2; // "; return str; } unserialize(data) { this.data = data; this.offset = 0; return this.unserializeAt(); } unserializeAt() { if (this.offset >= this.data.length) throw new Error('Expected type at '+this.offset); const type = this.data[this.offset].toLowerCase(); this.offset += 2; // t: let result, arraylength, typename; switch (type) { case 'i': return parseInt(this.readUntil(';'), 10); case 'b': return this.readUntil(';') !== '0'; case 'd': return parseFloat(this.readUntil(';')); case 'n': return null; case 's': return this.readStr(); case 'a': result = [ {}, [], true ]; arraylength = parseInt(this.readUntil(':')); this.offset++; for (let i = 0; i < arraylength; i++) { const key = this.unserializeAt(); const value = this.unserializeAt(); if (key != i) result[2] = false; if (result[2]) result[1][i] = value; result[0][key] = value; } this.offset++; return result[2] ? result[1] : result[0]; case 'o': result = {}; typename = this.readStr(); arraylength = parseInt(this.readUntil(':')); this.offset++; for (let i = 0; i < arraylength; i++) { let key = this.unserializeAt(); const value = this.unserializeAt(); key = key.replace('\u0000*\u0000', ''); result[key] = value; } this.offset++; return { [typename]: result }; default: throw new Error('Unknown / Unhandled data type(s): ' + type); } } }