'Null', self::TYPE_INTEGER => 'Integer', self::TYPE_FLOAT => 'Float', self::TYPE_STRING => 'String', self::TYPE_ARRAY => 'Array', self::TYPE_OBJECT => 'Object', ]; private static function isAssocArray($array): bool { if(!is_array($array) || $array === []) return false; return array_keys($array) !== range(0, count($array) - 1); } private static function detectType($data): int { if(is_null($data)) return self::TYPE_NULL; if(is_int($data)) return self::TYPE_INTEGER; if(is_float($data)) return self::TYPE_FLOAT; if(is_string($data)) // Should this check if a string is valid UTF-8 and swap over to TYPE_BUFFER? return self::TYPE_STRING; if(is_object($data) || self::isAssocArray($data)) return self::TYPE_OBJECT; if(is_array($data)) return self::TYPE_ARRAY; throw new FWIFUnsupportedTypeException(gettype($data)); } public static function encode($data): string { if($data instanceof FWIFSerializable) $data = $data->fwifSerialize(); $type = self::detectType($data); return chr($type) . self::{'encode' . self::CODECS[$type]}($data); } public static function decode(string $data) { return self::decodeInternal(new FWIFDecodeStream($data)); } private static function decodeInternal(FWIFDecodeStream $data) { $type = $data->readByte(); if(!array_key_exists($type, self::CODECS)) { $hexType = dechex($type); $hexPos = dechex($data->getPosition()); throw new FWIFUnsupportedTypeException("Unsupported type {$type} (0x{$hexType}) at position {$data->getPosition()} (0x{$hexPos})"); } return self::{'decode' . self::CODECS[$type]}($data); } private static function encodeNull($data): string { return ''; } private static function decodeNull(FWIFDecodeStream $data) { return null; } private static function encodeInteger(int $number): string { $packed = ''; $more = 1; $negative = $number < 0; $size = PHP_INT_SIZE * 8; while($more) { $byte = $number & 0x7F; $number >>= 7; if($negative) $number |= (~0 << ($size - 7)); if((!$number && !($byte & 0x40)) || ($number === -1 && ($byte & 0x40))) $more = 0; else $byte |= 0x80; $packed .= chr($byte); } return $packed; } private static function decodeInteger(FWIFDecodeStream $data): int { $number = 0; $shift = 0; $o = 0; $size = PHP_INT_SIZE * 8; do { $byte = $data->readByte(); $number |= ($byte & 0x7F) << $shift; $shift += 7; } while($byte & 0x80); if(($shift < $size) && ($byte & 0x40)) $number |= (~0 << $shift); return $number; } private static function encodeFloat(float $number): string { return pack('E', $number); } private static function decodeFloat(FWIFDecodeStream $data): float { $packed = ''; for($i = 0; $i < 8; ++$i) $packed .= chr($data->readByte()); return unpack('E', $packed)[1]; } private static function encodeString(string $string): string { $packed = ''; $string = unpack('C*', mb_convert_encoding($string, 'utf-8')); foreach($string as $char) $packed .= chr($char); return $packed . chr(self::TYPE_TRAILER); } private static function decodeAsciiString(FWIFDecodeStream $data): string { $string = ''; for(;;) { $byte = $data->readByte(); if($byte === self::TYPE_TRAILER) break; $string .= chr($byte); } return $string; } private static function decodeString(FWIFDecodeStream $data): string { // This should decode based on the utf-8 spec rather than just return mb_convert_encoding(self::decodeAsciiString($data), 'utf-8'); // grabbing the FF terminated string representation. } private static function encodeArray(array $array): string { $packed = ''; foreach($array as $value) $packed .= self::encode($value); return $packed . chr(self::TYPE_TRAILER); } private static function decodeArray(FWIFDecodeStream $data): array { $array = []; for(;;) { if($data->readByte() === self::TYPE_TRAILER) break; $data->stepBack(); $array[] = self::decodeInternal($data); } return $array; } private static function encodeObject($object): string { $packed = ''; $array = (array)$object; foreach($array as $name => $value) $packed .= $name . chr(self::TYPE_TRAILER) . self::encode($value); return $packed . chr(self::TYPE_TRAILER); } private static function decodeObject(FWIFDecodeStream $data): object { $array = []; for(;;) { if($data->readByte() === self::TYPE_TRAILER) break; $data->stepBack(); $name = self::decodeAsciiString($data); $array[$name] = self::decodeInternal($data); } return (object)$array; } }