Module refinery.units.formats.deserialize_php
Expand source code Browse git
from __future__ import annotations
from io import BytesIO
from refinery.lib import json
from refinery.units import Unit
class _phpobject:
__slots__ = ('__name__', '__php_vars__')
def __init__(self, name: str, d: dict | None = None):
object.__setattr__(self, '__name__', name)
object.__setattr__(self, '__php_vars__', d or {})
def _asdict(self) -> dict:
result = {}
for key, value in self.__php_vars__.items():
if key[:1] in (' ', '\x00'):
key = key.split(key[:1], 2)[-1]
result[key] = value
return result
class _PhpReader:
"""
Reads PHP serialized data from a stream. Like PHP, it numbers every value that it reads, except
for array keys and PHP references, so that the references `r` and `R` can name earlier values;
an array or object is numbered before its elements.
"""
def __init__(self, fp: BytesIO):
self.fp = fp
self.values: list[object] = []
def expect(self, e: bytes):
v = self.fp.read(len(e))
if v != e:
raise ValueError(F'expected {e!r}, got {v!r}')
def read_until(self, delim: bytes) -> bytes:
buf = []
while True:
char = self.fp.read(1)
if char == delim:
break
if not char:
raise ValueError('unexpected end of stream')
buf.append(char)
return b''.join(buf)
def load_array(self, result: dict):
"""
Reads the elements of an array or object into the given dictionary.
"""
count = int(self.read_until(b':'))
self.expect(b'{')
for _ in range(count):
key = self.unserialize(key=True)
result[key] = self.unserialize()
self.expect(b'}')
def unserialize(self, key: bool = False) -> object:
"""
Reads the next value; an array key is not numbered.
"""
opcode = self.fp.read(1)
if opcode in (b'r', b'R'):
self.expect(b':')
index = int(self.read_until(b';'))
if not 0 < index <= len(self.values):
raise ValueError(F'reference to value {index}, but only {len(self.values)} values were read')
value = self.values[index - 1]
if opcode == b'r' and not key:
self.values.append(value)
return value
slot = None
if not key:
slot = len(self.values)
self.values.append(None)
value = self._value(opcode.lower(), slot)
if slot is not None:
self.values[slot] = value
return value
def _value(self, opcode: bytes, slot: int | None) -> object:
if opcode == b'n':
self.expect(b';')
return None
if opcode in b'idb':
self.expect(b':')
value = self.read_until(b';')
if opcode == b'i':
return int(value)
if opcode == b'd':
return float(value)
return int(value) != 0
if opcode == b's':
self.expect(b':')
length = int(self.read_until(b':'))
self.expect(b'"')
value = self.fp.read(length)
self.expect(b'"')
self.expect(b';')
return value.decode('utf-8', 'surrogateescape')
if opcode == b'a':
self.expect(b':')
array = {}
if slot is not None:
self.values[slot] = array
self.load_array(array)
return array
if opcode == b'o':
self.expect(b':')
name_length = int(self.read_until(b':'))
self.expect(b'"')
name = self.fp.read(name_length).decode('utf-8', 'surrogateescape')
self.expect(b'":')
obj = _phpobject(name)
if slot is not None:
self.values[slot] = obj
self.load_array(obj.__php_vars__)
return obj
raise ValueError(F'unexpected opcode: {opcode!r}')
def _serialize(obj: object, keypos: bool) -> bytes:
"""
Serializes the given object in the PHP format, as an array key if `keypos` is set.
"""
if keypos:
if isinstance(obj, (int, float, bool)):
return F'i:{int(obj)};'.encode('latin1')
if isinstance(obj, str):
encoded = obj.encode('utf-8', 'surrogateescape')
return F's:{len(encoded)}:'.encode('latin1') + b'"' + encoded + b'";'
if isinstance(obj, bytes):
return F's:{len(obj)}:'.encode('latin1') + b'"' + obj + b'";'
if obj is None:
return b's:0:"";'
raise TypeError(F'cannot serialize {type(obj)!r} as key')
if obj is None:
return b'N;'
if isinstance(obj, bool):
return F'b:{int(obj)};'.encode('latin1')
if isinstance(obj, int):
return F'i:{obj};'.encode('latin1')
if isinstance(obj, float):
return F'd:{obj};'.encode('latin1')
if isinstance(obj, str):
encoded = obj.encode('utf-8', 'surrogateescape')
return F's:{len(encoded)}:'.encode('latin1') + b'"' + encoded + b'";'
if isinstance(obj, bytes):
return F's:{len(obj)}:'.encode('latin1') + b'"' + obj + b'";'
if isinstance(obj, dict):
parts = []
for key, value in obj.items():
parts.append(_serialize(key, True))
parts.append(_serialize(value, False))
return (
F'a:{len(obj)}:'.encode('latin1')
+ b'{' + b''.join(parts) + b'}'
)
if isinstance(obj, (list, tuple)):
parts = []
for index, value in enumerate(obj):
parts.append(_serialize(index, True))
parts.append(_serialize(value, False))
return (
F'a:{len(obj)}:'.encode('latin1')
+ b'{' + b''.join(parts) + b'}'
)
if isinstance(obj, _phpobject):
name = _serialize(obj.__name__, True)
body = _serialize(obj.__php_vars__, False)
return b'O' + name[1:-1] + body[1:]
raise TypeError(F'cannot serialize {type(obj)!r}')
class dsphp(Unit):
"""
Deserialize PHP serialized data and re-serialize as JSON.
"""
@staticmethod
def _loads(data: bytes | bytearray | memoryview) -> object:
return _PhpReader(BytesIO(bytes(data))).unserialize()
@staticmethod
def _dumps(data: object) -> bytes:
return _serialize(data, False)
def reverse(self, data):
return self._dumps(json.loads(data))
def process(self, data):
def tojson(obj):
if isinstance(obj, _phpobject):
return obj._asdict()
return json.dumps(self._loads(data), tojson=tojson)
Classes
class dsphp-
Deserialize PHP serialized data and re-serialize as JSON.
Expand source code Browse git
class dsphp(Unit): """ Deserialize PHP serialized data and re-serialize as JSON. """ @staticmethod def _loads(data: bytes | bytearray | memoryview) -> object: return _PhpReader(BytesIO(bytes(data))).unserialize() @staticmethod def _dumps(data: object) -> bytes: return _serialize(data, False) def reverse(self, data): return self._dumps(json.loads(data)) def process(self, data): def tojson(obj): if isinstance(obj, _phpobject): return obj._asdict() return json.dumps(self._loads(data), tojson=tojson)Ancestors
Subclasses
Inherited members