Add initial implementation of PHP's (un)?serialize(Session)?
All other implementations miss somethingmaster
commit
f1d9f504ad
|
@ -0,0 +1,237 @@
|
|||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue