'Null', self::TYPE_INTEGER => 'Integer', self::TYPE_FLOAT => 'Float', self::TYPE_STRING => 'String', self::TYPE_ARRAY => 'Array', self::TYPE_OBJECT => 'Object', self::TYPE_BUFFER => 'Buffer', self::TYPE_DATETIME => 'DateTime', self::TYPE_TIMESPAN => 'TimeSpan', ]; private const UTF8 = '%^(?:' // https://www.w3.org/International/questions/qa-forms-utf-8.en . '[\x09\x0A\x0D\x20-\x7E]' // ASCII . '|[\xC2-\xDF][\x80-\xBF]' // non-overlong 2-byte . '|\xE0[\xA0-\xBF][\x80-\xBF]' // excluding overlongs . '|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}' // straight 3-byte . '|\xED[\x80-\x9F][\x80-\xBF]' // excluding surrogates . '|\xF0[\x90-\xBF][\x80-\xBF]{2}' // planes 1-3 . '|[\xF1-\xF3][\x80-\xBF]{3}' // planes 4-15 . '|\xF4[\x80-\x8F][\x80-\xBF]{2}' // plane 16 . ')*$%xs'; private static function isAssocArray($array): bool { if(!is_array($array) || $array === []) return false; return array_keys($array) !== range(0, count($array) - 1); } // apparently this is faster than mb_check_encoding($string, 'utf-8'); // on PHP 7.1 on Windows at least, perhaps investigate this later // UPDATE TODO: does this even make any sense with other internal encodings? private static function isUTF8String(string $string): bool { return preg_match(self::UTF8, $string) === 1; } private static function detectType($data, int $flags): 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)) return self::isUTF8String($data) ? self::TYPE_STRING : self::TYPE_BUFFER; if(is_object($data) || self::isAssocArray($data)) { if($data instanceof DateTimeInterface) return self::TYPE_DATETIME; if($data instanceof DateInterval) return self::TYPE_TIMESPAN; return self::TYPE_OBJECT; } if(is_array($data)) return self::TYPE_ARRAY; throw new FWIFUnsupportedTypeException(gettype($data)); } public static function encode($data, int $flags = self::DEFAULT): string { $encoded = self::encodeInternal($data, $flags); if(!($flags & self::EXCLUDE_VERSION)) $encoded = chr(self::VERSION) . $encoded; return $encoded; } private static function encodeInternal($data, int $flags): string { if($data instanceof FWIFSerializable) $data = $data->fwifSerialize(); $type = self::detectType($data, $flags); return chr($type) . self::{'encode' . self::CODECS[$type]}($data, $flags); } public static function decode($data, int $flags = self::DEFAULT) { if(is_string($data)) { $fd = fopen('php://memory', 'rb+'); fwrite($fd, $data); fseek($fd, 0); $data = $fd; } if(!is_resource($data)) throw new InvalidArgumentException('$data must be either a string or a file handle.'); if(!($flags & self::EXCLUDE_VERSION)) { $version = ord(fgetc($data)); if($version < 1 || $version > 254) throw new InvalidArgumentException('$data is not a valid FWIF serialized stream.'); if($version > self::VERSION) throw new FWIFUnsupportedVersionException; } $decoded = self::decodeInternal($data, $flags); if(isset($fd)) fclose($fd); return $decoded; } private static function decodeInternal($data, int $flags) { $type = ord(fgetc($data)); if(!array_key_exists($type, self::CODECS)) { $hexType = dechex($type); $pos = ftell($data); $hexPos = dechex($pos); throw new FWIFUnsupportedTypeException("Unsupported type {$type} (0x{$hexType}) at position {$pos} (0x{$hexPos})"); } return self::{'decode' . self::CODECS[$type]}($data, $flags); } private static function encodeNull($data, int $flags): string { return ''; } private static function decodeNull($data, int $flags) { return null; } private static function encodeInteger(int $number, int $flags): 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($data, int $flags): int { $number = 0; $shift = 0; $size = PHP_INT_SIZE * 8; do { $byte = ord(fgetc($data)); $number |= ($byte & 0x7F) << $shift; $shift += 7; } while($byte & 0x80); if(($shift < $size) && ($byte & 0x40)) $number |= (~0 << $shift); return $number; } private const FLOAT_MZ = 0x04; private const FLOAT_EZ = 0x08; private const FLOAT_ZERO = self::FLOAT_MZ | self::FLOAT_EZ; private const FLOAT_NAN = 0x20; private const FLOAT_INF = 0x40; private const FLOAT_SIGN = 0x80; private const FLOAT_NINF = self::FLOAT_INF | self::FLOAT_SIGN; private const FLOAT_DIV = 0x7FFFFFFF; private static function encodeFloat(float $number, int $flags): string { if(is_nan($number)) $packed = chr(self::FLOAT_NAN); elseif($number === INF) $packed = chr(self::FLOAT_INF); elseif($number === -INF) $packed = chr(self::FLOAT_NINF); else { $byte1 = 0; $packed = ''; if($number < 0.0 || ($number ** -1 === -INF)) $byte1 |= self::FLOAT_SIGN; if($number === 0.0) $byte1 |= self::FLOAT_ZERO; else { $numAbs = abs($number); $exp = (int)(floor(log($numAbs, 2)) + 1); $man = (int)(($numAbs * (2 ** -$exp)) * self::FLOAT_DIV); if($exp != 0) $packed .= self::encodeInteger($exp, $flags); else $byte1 |= self::FLOAT_EZ; if($man > 0) $packed .= pack('N', $man); else $byte1 |= self::FLOAT_MZ; } $packed = chr($byte1) . $packed; } return $packed; } private static function decodeFloat($data, int $flags): float { $byte1 = ord(fgetc($data)); if($byte1 & self::FLOAT_NAN) return NAN; if($byte1 & self::FLOAT_INF) return ($byte1 & self::FLOAT_SIGN) ? -INF : INF; if(($byte1 & self::FLOAT_ZERO) === self::FLOAT_ZERO) return ($byte1 & self::FLOAT_SIGN) ? -0.0 : 0.0; $exp = 0; $man = 0; if(!($byte1 & self::FLOAT_EZ)) $exp = self::decodeInteger($data, $flags); if(!($byte1 & self::FLOAT_MZ)) $man = ((float)unpack('N', fread($data, 4))[1]) / self::FLOAT_DIV; $number = $man * (2 ** $exp); if($byte1 & self::FLOAT_SIGN) $number *= -1; return $number; } private static function encodeString(string $string, int $flags): string { $packed = ''; $string = unpack('C*', mb_convert_encoding($string, 'utf-8', mb_internal_encoding())); foreach($string as $char) $packed .= chr($char); return $packed . chr(self::TRAILER); } private static function decodeString($data, int $flags): string { $packed = ''; for(;;) { $char = fgetc($data); $byte = ord($char); if($byte == self::TRAILER) break; $packed .= $char; if(($byte & 0xF8) == 0xF0) $packed .= fread($data, 3); elseif(($byte & 0xF0) == 0xE0) $packed .= fread($data, 2); elseif(($byte & 0xE0) == 0xC0) $packed .= fgetc($data); } return mb_convert_encoding($packed, mb_internal_encoding(), 'utf-8'); } private static function encodeArray(array $array, int $flags): string { $packed = ''; foreach($array as $value) $packed .= self::encodeInternal($value, $flags); return $packed . chr(self::TRAILER); } private static function decodeArray($data, int $flags): array { $array = []; for(;;) { if(ord(fgetc($data)) === self::TRAILER) break; fseek($data, -1, SEEK_CUR); $array[] = self::decodeInternal($data, $flags); } return $array; } private static function encodeObject($object, int $flags): string { $packed = ''; $array = (array)$object; foreach($array as $name => $value) $packed .= mb_convert_encoding($name, 'us-ascii', mb_internal_encoding()) . chr(self::TRAILER) . self::encodeInternal($value, $flags); return $packed . chr(self::TRAILER); } private static function decodeObjectKey($data, int $flags): string { $packed = ''; for(;;) { $char = fgetc($data); if(ord($char) === self::TRAILER) break; $packed .= $char; } return mb_convert_encoding($packed, mb_internal_encoding(), 'us-ascii'); } private static function decodeObject($data, int $flags): object { $array = []; for(;;) { if(ord(fgetc($data)) === self::TRAILER) break; fseek($data, -1, SEEK_CUR); $array[self::decodeObjectKey($data, $flags)] = self::decodeInternal($data, $flags); } return (object)$array; } private static function encodeBuffer(string $buffer, int $flags): string { return self::encodeInteger(strlen($buffer), $flags) . $buffer; } private static function decodeBuffer($data, int $flags): string { return fread($data, self::decodeInteger($data, $flags)); } private const DATETIME_FLAG_TIME = 0x40; private const DATETIME_FLAG_MILLI = 0x4000; private const DATETIME_YEAR_SIGN = 0x40000000; private const DATETIME_YEAR_MASK = 0x3FFF; private const DATETIME_YEAR_SHIFT = 16; // << private const DATETIME_MONTH_MASK = 0x0F; private const DATETIME_MONTH_SHIFT = 12; // << private const DATETIME_DAY_MASK = 0x1F; private const DATETIME_DAY_SHIFT = 7; // << private const DATETIME_HOUR_MASK = 0x1F; private const DATETIME_MINS_MASK = 0x3F; private const DATETIME_MINS_SHIFT = 8; // << private const DATETIME_SECS_MASK = 0x3F; private const DATETIME_SECS_SHIFT = 2; // << private const DATETIME_MILLI_HI_MASK = 0x300; private const DATETIME_MILLI_HI_SHIFT = 8; // >> private const DATETIME_MILLI_LO_MASK = 0x0FF; /* +--------+--------+ Y - Signed 15-bit year W - w enable flag * |.YYYYYYY|YYYYYYYY| M - Unsigned 4-bit month m - unsigned 6-bit minutes * |MMMMDDDD|DT.HHHHH| D - Unsigned 5-bit Day S - unsigned 6-bit seconds * |.Wmmmmmm|SSSSSSww| T - WmSw enable flag w - unsigned 10-bit millisecs * |wwwwwwww| | H - Unsigned 5-bit hours * +--------+--------+ */ private static function encodeDateTime(DateTimeInterface $dt, int $flags): string { static $utc = null; if($utc === null) $utc = new DateTimeZone('utc'); if($dt->getTimezone()->getOffset($dt) !== 0) $dt = DateTime::createFromInterface($dt)->setTimezone($utc); $year = (int)$dt->format('Y'); $month = (int)$dt->format('n'); $day = (int)$dt->format('j'); $hours = (int)$dt->format('G'); $mins = (int)$dt->format('i'); $secs = (int)$dt->format('s'); $millis = ($flags & self::DISCARD_MILLISECONDS) ? 0 : (int)$dt->format('v'); $subYear = $year < 0; if($subYear) $year = ~$year; $ymdh = $subYear ? self::DATETIME_YEAR_SIGN : 0; $ymdh |= ($year & self::DATETIME_YEAR_MASK) << self::DATETIME_YEAR_SHIFT; $ymdh |= ($month & self::DATETIME_MONTH_MASK) << self::DATETIME_MONTH_SHIFT; $ymdh |= ($day & self::DATETIME_DAY_MASK) << self::DATETIME_DAY_SHIFT; $ymdh |= ($hours & self::DATETIME_HOUR_MASK); if($mins > 0 || $secs > 0 || $millis > 0) { $ymdh |= self::DATETIME_FLAG_TIME; $msw = 0; $msw |= ($mins & self::DATETIME_MINS_MASK) << self::DATETIME_MINS_SHIFT; $msw |= ($secs & self::DATETIME_SECS_MASK) << self::DATETIME_SECS_SHIFT; if($millis > 0) { $msw |= self::DATETIME_FLAG_MILLI; $msw |= ($millis & self::DATETIME_MILLI_HI_MASK) >> self::DATETIME_MILLI_HI_SHIFT; $w = $millis & self::DATETIME_MILLI_LO_MASK; } } $packed = pack('N', $ymdh); if($ymdh & self::DATETIME_FLAG_TIME) { $packed .= pack('n', $msw); if($msw & self::DATETIME_FLAG_MILLI) $packed .= chr($w); } return $packed; } private static function decodeDateTime($data, int $flags): DateTimeInterface { $ymdh = unpack('N', fread($data, 4))[1]; $years = ($ymdh >> self::DATETIME_YEAR_SHIFT) & self::DATETIME_YEAR_MASK; $months = ($ymdh >> self::DATETIME_MONTH_SHIFT) & self::DATETIME_MONTH_MASK; $days = ($ymdh >> self::DATETIME_DAY_SHIFT) & self::DATETIME_DAY_MASK; $hours = $ymdh & self::DATETIME_HOUR_MASK; if($ymdh & self::DATETIME_YEAR_SIGN) $years = ~$years; $dt = sprintf('%04d-%02d-%02dT%02d:', $years, $months, $days, $hours); if($ymdh & self::DATETIME_FLAG_TIME) { $msw = unpack('n', fread($data, 2))[1]; $mins = ($msw >> self::DATETIME_MINS_SHIFT) & self::DATETIME_MINS_MASK; $secs = ($msw >> self::DATETIME_SECS_SHIFT) & self::DATETIME_SECS_MASK; $dt .= sprintf('%02d:%02d', $mins, $secs); if($msw & self::DATETIME_FLAG_MILLI) { $millis = ($msw << self::DATETIME_MILLI_HI_SHIFT) & self::DATETIME_MILLI_HI_MASK; $millis |= ord(fgetc($data)); $dt .= sprintf('.%03d', $millis); } } else $dt .= '00:00'; return new DateTimeImmutable($dt . 'UTC'); } private const TIMESPAN_FLAG_DAYS = 0x20000000; private const TIMESPAN_FLAG_NEGA = 0x40000000; private const TIMESPAN_MILLI_SHIFT = 16; // << private const TIMESPAN_MILLI_MASK = 0x3FF; private const TIMESPAN_SECS_SHIFT = 11; // << private const TIMESPAN_MINS_SHIFT = 5; // << private const TIMESPAN_DAYS_MASK = 0x7FFF; private const TIMESPAN_YEAR_DAYS = 365.0; private const TIMESPAN_MONTH_DAYS = 31.0; /* +--------+--------+ w - unsigned 10-bit millisecs n - Negative flag * |.nd..www|wwwwwwwS| S - unsigned 6-bit seconds d - DMY enable flag * |SSSSSmmm|mmmHHHHH| m - unsigned 6-bit minutes D - unsigned 15-bit day * |.DDDDDDD|DDDDDDDD| H - unsigned 5-bit hours * +--------+--------+ */ private static function encodeTimeSpan(DateInterval $di, int $flags): string { $wsmh = $di->invert ? self::TIMESPAN_FLAG_NEGA : 0; $wsmh |= ((int)($di->f * 1000) & self::TIMESPAN_MILLI_MASK) << self::TIMESPAN_MILLI_SHIFT; $wsmh |= ( $di->s & self::DATETIME_SECS_MASK) << self::TIMESPAN_SECS_SHIFT; $wsmh |= ( $di->i & self::DATETIME_MINS_MASK) << self::TIMESPAN_MINS_SHIFT; $wsmh |= ( $di->h & self::DATETIME_HOUR_MASK); $days = $di->days; if($days === false) // Best I can come up with on short notice, fuck it $days = (int)($di->d + ($di->m * self::TIMESPAN_YEAR_DAYS) + ($di->y * self::TIMESPAN_MONTH_DAYS)); if($days > 0) { $wsmh |= self::TIMESPAN_FLAG_DAYS; $days &= self::TIMESPAN_DAYS_MASK; } $packed = pack('N', $wsmh); if($wsmh & self::TIMESPAN_FLAG_DAYS) $packed .= pack('n', $days); return $packed; } private static function decodeTimeSpan($data, int $flags): DateInterval { $di = new DateInterval('P0Y'); $wsmh = unpack('N', fread($data, 4))[1]; $di->invert = ($wsmh & self::TIMESPAN_FLAG_NEGA) ? 1 : 0; $di->f = (($wsmh >> self::TIMESPAN_MILLI_SHIFT) & self::TIMESPAN_MILLI_MASK) / 1000.0; $di->s = ($wsmh >> self::TIMESPAN_SECS_SHIFT) & self::DATETIME_SECS_MASK; $di->i = ($wsmh >> self::TIMESPAN_MINS_SHIFT) & self::DATETIME_MINS_MASK; $di->h = $wsmh & self::DATETIME_HOUR_MASK; if($wsmh & self::TIMESPAN_FLAG_DAYS) { $days = unpack('n', fread($data, 2))[1] & self::TIMESPAN_DAYS_MASK; $di->days = $days; $di->y = (int)floor($days / self::TIMESPAN_YEAR_DAYS); $days -= $di->y * self::TIMESPAN_YEAR_DAYS; $di->m = (int)ceil($days / self::TIMESPAN_MONTH_DAYS); $di->d = max(0, $days - ($di->m * self::TIMESPAN_MONTH_DAYS)); } return $di; } }