162 lines
6.5 KiB
PHP
162 lines
6.5 KiB
PHP
<?php
|
|
namespace FWIF;
|
|
|
|
class FWIF {
|
|
public const CONTENT_TYPE = 'text/plain; charset=us-ascii'; // TODO: come up with a mime type
|
|
|
|
public const TYPE_NULL = 0; // NULL, no data
|
|
public const TYPE_INTEGER = 0x01; // LEB128, implicit length
|
|
public const TYPE_FLOAT = 0x02; // double precision IEEE 754, fixed length of 8 bytes
|
|
public const TYPE_STRING = 0x03; // UTF-8 string, terminated with TYPE_TRAILER
|
|
public const TYPE_ARRAY = 0x04; // List of values, terminated with TYPE_TRAILER
|
|
public const TYPE_OBJECT = 0x05; // List of values with ASCII names, terminated with TYPE_TRAILER
|
|
public const TYPE_BUFFER = 0x06; // Buffer with binary data, prefixed with a LEB128 length
|
|
public const TYPE_DATE = 0x07; // A gregorian year, month and day, fixed length of * bytes
|
|
public const TYPE_DATETIME = 0x08; // A gregorian year, month and day as well as an hour, minute and seconds component, fixed length of * bytes
|
|
public const TYPE_PERIOD = 0x09; // A time period, fixed length of * bytes
|
|
public const TYPE_TRAILER = 0xFF; // Termination byte
|
|
|
|
private const CODECS = [
|
|
self::TYPE_NULL => '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;
|
|
}
|
|
}
|