| 1 |
<?php |
| 2 |
|
| 3 |
namespace Dudlewebs\WPMCS\s3\Aws\Api\Parser; |
| 4 |
|
| 5 |
use Iterator; |
| 6 |
use Dudlewebs\WPMCS\s3\Aws\Api\DateTimeResult; |
| 7 |
use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7; |
| 8 |
use Dudlewebs\WPMCS\s3\Psr\Http\Message\StreamInterface; |
| 9 |
use Dudlewebs\WPMCS\s3\Aws\Api\Parser\Exception\ParserException; |
| 10 |
/** |
| 11 |
* @internal Implements a decoder for a binary encoded event stream that will |
| 12 |
* decode, validate, and provide individual events from the stream. |
| 13 |
*/ |
| 14 |
class DecodingEventStreamIterator implements Iterator |
| 15 |
{ |
| 16 |
const HEADERS = 'headers'; |
| 17 |
const PAYLOAD = 'payload'; |
| 18 |
const LENGTH_TOTAL = 'total_length'; |
| 19 |
const LENGTH_HEADERS = 'headers_length'; |
| 20 |
const CRC_PRELUDE = 'prelude_crc'; |
| 21 |
const BYTES_PRELUDE = 12; |
| 22 |
const BYTES_TRAILING = 4; |
| 23 |
private static $preludeFormat = [self::LENGTH_TOTAL => 'decodeUint32', self::LENGTH_HEADERS => 'decodeUint32', self::CRC_PRELUDE => 'decodeUint32']; |
| 24 |
private static $lengthFormatMap = [1 => 'decodeUint8', 2 => 'decodeUint16', 4 => 'decodeUint32', 8 => 'decodeUint64']; |
| 25 |
private static $headerTypeMap = [0 => 'decodeBooleanTrue', 1 => 'decodeBooleanFalse', 2 => 'decodeInt8', 3 => 'decodeInt16', 4 => 'decodeInt32', 5 => 'decodeInt64', 6 => 'decodeBytes', 7 => 'decodeString', 8 => 'decodeTimestamp', 9 => 'decodeUuid']; |
| 26 |
/** @var StreamInterface Stream of eventstream shape to parse. */ |
| 27 |
protected $stream; |
| 28 |
/** @var array Currently parsed event. */ |
| 29 |
protected $currentEvent; |
| 30 |
/** @var int Current in-order event key. */ |
| 31 |
protected $key; |
| 32 |
/** @var resource|\HashContext CRC32 hash context for event validation */ |
| 33 |
protected $hashContext; |
| 34 |
/** @var int $currentPosition */ |
| 35 |
protected $currentPosition; |
| 36 |
/** |
| 37 |
* DecodingEventStreamIterator constructor. |
| 38 |
* |
| 39 |
* @param StreamInterface $stream |
| 40 |
*/ |
| 41 |
public function __construct(StreamInterface $stream) |
| 42 |
{ |
| 43 |
$this->stream = $stream; |
| 44 |
$this->rewind(); |
| 45 |
} |
| 46 |
protected function parseHeaders($headerBytes) |
| 47 |
{ |
| 48 |
$headers = []; |
| 49 |
$bytesRead = 0; |
| 50 |
while ($bytesRead < $headerBytes) { |
| 51 |
list($key, $numBytes) = $this->decodeString(1); |
| 52 |
$bytesRead += $numBytes; |
| 53 |
list($type, $numBytes) = $this->decodeUint8(); |
| 54 |
$bytesRead += $numBytes; |
| 55 |
$f = self::$headerTypeMap[$type]; |
| 56 |
list($value, $numBytes) = $this->{$f}(); |
| 57 |
$bytesRead += $numBytes; |
| 58 |
if (isset($headers[$key])) { |
| 59 |
throw new ParserException('Duplicate key in event headers.'); |
| 60 |
} |
| 61 |
$headers[$key] = $value; |
| 62 |
} |
| 63 |
return [$headers, $bytesRead]; |
| 64 |
} |
| 65 |
protected function parsePrelude() |
| 66 |
{ |
| 67 |
$prelude = []; |
| 68 |
$bytesRead = 0; |
| 69 |
$calculatedCrc = null; |
| 70 |
foreach (self::$preludeFormat as $key => $decodeFunction) { |
| 71 |
if ($key === self::CRC_PRELUDE) { |
| 72 |
$hashCopy = \hash_copy($this->hashContext); |
| 73 |
$calculatedCrc = \hash_final($this->hashContext, \true); |
| 74 |
$this->hashContext = $hashCopy; |
| 75 |
} |
| 76 |
list($value, $numBytes) = $this->{$decodeFunction}(); |
| 77 |
$bytesRead += $numBytes; |
| 78 |
$prelude[$key] = $value; |
| 79 |
} |
| 80 |
if (\unpack('N', $calculatedCrc)[1] !== $prelude[self::CRC_PRELUDE]) { |
| 81 |
throw new ParserException('Prelude checksum mismatch.'); |
| 82 |
} |
| 83 |
return [$prelude, $bytesRead]; |
| 84 |
} |
| 85 |
/** |
| 86 |
* This method decodes an event from the stream. |
| 87 |
* |
| 88 |
* @return array |
| 89 |
*/ |
| 90 |
protected function parseEvent() |
| 91 |
{ |
| 92 |
$event = []; |
| 93 |
if ($this->stream->tell() < $this->stream->getSize()) { |
| 94 |
$this->hashContext = \hash_init('crc32b'); |
| 95 |
$bytesLeft = $this->stream->getSize() - $this->stream->tell(); |
| 96 |
list($prelude, $numBytes) = $this->parsePrelude(); |
| 97 |
if ($prelude[self::LENGTH_TOTAL] > $bytesLeft) { |
| 98 |
throw new ParserException('Message length too long.'); |
| 99 |
} |
| 100 |
$bytesLeft -= $numBytes; |
| 101 |
if ($prelude[self::LENGTH_HEADERS] > $bytesLeft) { |
| 102 |
throw new ParserException('Headers length too long.'); |
| 103 |
} |
| 104 |
list($event[self::HEADERS], $numBytes) = $this->parseHeaders($prelude[self::LENGTH_HEADERS]); |
| 105 |
$event[self::PAYLOAD] = Psr7\Utils::streamFor($this->readAndHashBytes($prelude[self::LENGTH_TOTAL] - self::BYTES_PRELUDE - $numBytes - self::BYTES_TRAILING)); |
| 106 |
$calculatedCrc = \hash_final($this->hashContext, \true); |
| 107 |
$messageCrc = $this->stream->read(4); |
| 108 |
if ($calculatedCrc !== $messageCrc) { |
| 109 |
throw new ParserException('Message checksum mismatch.'); |
| 110 |
} |
| 111 |
} |
| 112 |
return $event; |
| 113 |
} |
| 114 |
// Iterator Functionality |
| 115 |
/** |
| 116 |
* @return array |
| 117 |
*/ |
| 118 |
#[\ReturnTypeWillChange] |
| 119 |
public function current() |
| 120 |
{ |
| 121 |
return $this->currentEvent; |
| 122 |
} |
| 123 |
/** |
| 124 |
* @return int |
| 125 |
*/ |
| 126 |
#[\ReturnTypeWillChange] |
| 127 |
public function key() |
| 128 |
{ |
| 129 |
return $this->key; |
| 130 |
} |
| 131 |
/** |
| 132 |
* @return void |
| 133 |
*/ |
| 134 |
#[\ReturnTypeWillChange] |
| 135 |
public function next() |
| 136 |
{ |
| 137 |
$this->currentPosition = $this->stream->tell(); |
| 138 |
if ($this->valid()) { |
| 139 |
$this->key++; |
| 140 |
$this->currentEvent = $this->parseEvent(); |
| 141 |
} |
| 142 |
} |
| 143 |
/** |
| 144 |
* @return void |
| 145 |
*/ |
| 146 |
#[\ReturnTypeWillChange] |
| 147 |
public function rewind() |
| 148 |
{ |
| 149 |
$this->stream->rewind(); |
| 150 |
$this->key = 0; |
| 151 |
$this->currentPosition = 0; |
| 152 |
$this->currentEvent = $this->parseEvent(); |
| 153 |
} |
| 154 |
/** |
| 155 |
* @return bool |
| 156 |
*/ |
| 157 |
#[\ReturnTypeWillChange] |
| 158 |
public function valid() |
| 159 |
{ |
| 160 |
return $this->currentPosition < $this->stream->getSize(); |
| 161 |
} |
| 162 |
// Decoding Utilities |
| 163 |
protected function readAndHashBytes($num) |
| 164 |
{ |
| 165 |
$bytes = $this->stream->read($num); |
| 166 |
\hash_update($this->hashContext, $bytes); |
| 167 |
return $bytes; |
| 168 |
} |
| 169 |
private function decodeBooleanTrue() |
| 170 |
{ |
| 171 |
return [\true, 0]; |
| 172 |
} |
| 173 |
private function decodeBooleanFalse() |
| 174 |
{ |
| 175 |
return [\false, 0]; |
| 176 |
} |
| 177 |
private function uintToInt($val, $size) |
| 178 |
{ |
| 179 |
$signedCap = \pow(2, $size - 1); |
| 180 |
if ($val > $signedCap) { |
| 181 |
$val -= 2 * $signedCap; |
| 182 |
} |
| 183 |
return $val; |
| 184 |
} |
| 185 |
private function decodeInt8() |
| 186 |
{ |
| 187 |
$val = (int) \unpack('C', $this->readAndHashBytes(1))[1]; |
| 188 |
return [$this->uintToInt($val, 8), 1]; |
| 189 |
} |
| 190 |
private function decodeUint8() |
| 191 |
{ |
| 192 |
return [\unpack('C', $this->readAndHashBytes(1))[1], 1]; |
| 193 |
} |
| 194 |
private function decodeInt16() |
| 195 |
{ |
| 196 |
$val = (int) \unpack('n', $this->readAndHashBytes(2))[1]; |
| 197 |
return [$this->uintToInt($val, 16), 2]; |
| 198 |
} |
| 199 |
private function decodeUint16() |
| 200 |
{ |
| 201 |
return [\unpack('n', $this->readAndHashBytes(2))[1], 2]; |
| 202 |
} |
| 203 |
private function decodeInt32() |
| 204 |
{ |
| 205 |
$val = (int) \unpack('N', $this->readAndHashBytes(4))[1]; |
| 206 |
return [$this->uintToInt($val, 32), 4]; |
| 207 |
} |
| 208 |
private function decodeUint32() |
| 209 |
{ |
| 210 |
return [\unpack('N', $this->readAndHashBytes(4))[1], 4]; |
| 211 |
} |
| 212 |
private function decodeInt64() |
| 213 |
{ |
| 214 |
$val = $this->unpackInt64($this->readAndHashBytes(8))[1]; |
| 215 |
return [$this->uintToInt($val, 64), 8]; |
| 216 |
} |
| 217 |
private function decodeUint64() |
| 218 |
{ |
| 219 |
return [$this->unpackInt64($this->readAndHashBytes(8))[1], 8]; |
| 220 |
} |
| 221 |
private function unpackInt64($bytes) |
| 222 |
{ |
| 223 |
return \unpack('J', $bytes); |
| 224 |
} |
| 225 |
private function decodeBytes($lengthBytes = 2) |
| 226 |
{ |
| 227 |
if (!isset(self::$lengthFormatMap[$lengthBytes])) { |
| 228 |
throw new ParserException('Undefined variable length format.'); |
| 229 |
} |
| 230 |
$f = self::$lengthFormatMap[$lengthBytes]; |
| 231 |
list($len, $bytes) = $this->{$f}(); |
| 232 |
return [$this->readAndHashBytes($len), $len + $bytes]; |
| 233 |
} |
| 234 |
private function decodeString($lengthBytes = 2) |
| 235 |
{ |
| 236 |
if (!isset(self::$lengthFormatMap[$lengthBytes])) { |
| 237 |
throw new ParserException('Undefined variable length format.'); |
| 238 |
} |
| 239 |
$f = self::$lengthFormatMap[$lengthBytes]; |
| 240 |
list($len, $bytes) = $this->{$f}(); |
| 241 |
return [$this->readAndHashBytes($len), $len + $bytes]; |
| 242 |
} |
| 243 |
private function decodeTimestamp() |
| 244 |
{ |
| 245 |
list($val, $bytes) = $this->decodeInt64(); |
| 246 |
return [DateTimeResult::createFromFormat('U.u', $val / 1000), $bytes]; |
| 247 |
} |
| 248 |
private function decodeUuid() |
| 249 |
{ |
| 250 |
$val = \unpack('H32', $this->readAndHashBytes(16))[1]; |
| 251 |
return [\substr($val, 0, 8) . '-' . \substr($val, 8, 4) . '-' . \substr($val, 12, 4) . '-' . \substr($val, 16, 4) . '-' . \substr($val, 20, 12), 16]; |
| 252 |
} |
| 253 |
} |
| 254 |
|