class-basebytereadstream.php
1 week ago
class-deflatereadstream.php
1 week ago
class-filereadstream.php
1 week ago
class-inflatereadstream.php
1 week ago
class-limitedbytereadstream.php
1 week ago
class-transformedreadstream.php
1 week ago
interface-bytereadstream.php
1 week ago
class-basebytereadstream.php
239 lines
| 1 | <?php |
| 2 | |
| 3 | namespace WordPress\ByteStream\ReadStream; |
| 4 | |
| 5 | use WordPress\ByteStream\ByteStreamException; |
| 6 | use WordPress\ByteStream\NotEnoughDataException; |
| 7 | |
| 8 | abstract class BaseByteReadStream implements ByteReadStream { |
| 9 | |
| 10 | const CHUNK_SIZE_BYTES = 64 * 1024; // 64kb. |
| 11 | |
| 12 | /** |
| 13 | * The maximum number of consumed bytes to keep in memory. |
| 14 | * |
| 15 | * For example: |
| 16 | * |
| 17 | * The quick brown fox jumps over the lazy dog. |
| 18 | * ^-------------------^ |
| 19 | * consumed bytes |
| 20 | * |
| 21 | * Say the maximum lookbehind bytes is 4. Then the byte stream will forget about |
| 22 | * all consumed bytes except the last 4: |
| 23 | * |
| 24 | * fox jumps over the lazy dog. |
| 25 | * ^--^ |
| 26 | * consumed but retained for seek()-ing backwards. |
| 27 | * |
| 28 | * @var int |
| 29 | */ |
| 30 | protected $max_lookbehind_bytes = 2048; |
| 31 | |
| 32 | /** |
| 33 | * The remaining unconsumed bytes. |
| 34 | * |
| 35 | * @var string |
| 36 | */ |
| 37 | protected $buffer = ''; |
| 38 | |
| 39 | /** |
| 40 | * How many bytes have already been consumed in the current **buffer**. |
| 41 | * |
| 42 | * @var int |
| 43 | */ |
| 44 | protected $offset_in_current_buffer = 0; |
| 45 | |
| 46 | /** |
| 47 | * How many bytes have already been forgotten in the current **stream**. |
| 48 | * |
| 49 | * @var int |
| 50 | */ |
| 51 | protected $bytes_already_forgotten = 0; |
| 52 | |
| 53 | /** |
| 54 | * Whether the stream has been closed for reading. |
| 55 | * |
| 56 | * @var bool |
| 57 | */ |
| 58 | protected $is_read_closed = false; |
| 59 | |
| 60 | /** |
| 61 | * How many bytes are expected in the stream. Optional. |
| 62 | * |
| 63 | * When it's null, the stream is unbounded and length() will also return null. |
| 64 | * |
| 65 | * @var int|null |
| 66 | */ |
| 67 | protected $expected_length = null; |
| 68 | |
| 69 | public function length(): ?int { |
| 70 | return $this->expected_length; |
| 71 | } |
| 72 | |
| 73 | public function pull( ?int $n = self::CHUNK_SIZE_BYTES, string $mode = self::PULL_NO_MORE_THAN ): int { |
| 74 | switch ( $mode ) { |
| 75 | case self::PULL_NO_MORE_THAN: |
| 76 | case self::PULL_EXACTLY: |
| 77 | break; |
| 78 | default: |
| 79 | throw new ByteStreamException( 'Invalid pull mode' ); |
| 80 | } |
| 81 | if ( $this->is_read_closed ) { |
| 82 | throw new ByteStreamException( 'Cannot pull() on a closed producer' ); |
| 83 | } |
| 84 | |
| 85 | if ( 0 === $n ) { |
| 86 | return 0; |
| 87 | } |
| 88 | |
| 89 | if ( $n < 0 ) { |
| 90 | throw new ByteStreamException( 'Cannot pull a negative number of bytes' ); |
| 91 | } |
| 92 | |
| 93 | if ( $n <= $this->count_consumable_bytes() ) { |
| 94 | return $n; |
| 95 | } |
| 96 | |
| 97 | if ( $this->reached_end_of_data() ) { |
| 98 | if ( ByteReadStream::PULL_EXACTLY === $mode ) { |
| 99 | throw new NotEnoughDataException( 'End of data reached while pulling' ); |
| 100 | } |
| 101 | |
| 102 | return 0; |
| 103 | } |
| 104 | |
| 105 | if ( ByteReadStream::PULL_NO_MORE_THAN === $mode ) { |
| 106 | return $this->pull_no_more_than( $n ); |
| 107 | } |
| 108 | |
| 109 | return $this->pull_exactly( $n ); |
| 110 | } |
| 111 | |
| 112 | protected function pull_exactly( $n ): int { |
| 113 | $empty_pulls = 0; |
| 114 | while ( $this->count_consumable_bytes() < $n ) { |
| 115 | $consumable_before = $this->count_consumable_bytes(); |
| 116 | $this->pull_no_more_than( $n ); |
| 117 | $consumable_after = $this->count_consumable_bytes(); |
| 118 | |
| 119 | if ( $consumable_after === $consumable_before ) { |
| 120 | ++$empty_pulls; |
| 121 | if ( $this->reached_end_of_data() ) { |
| 122 | throw new NotEnoughDataException( 'End of data reached while pulling' ); |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | if ( $empty_pulls > 4 ) { |
| 127 | throw new NotEnoughDataException( '4 empty pulls in a row, we are probably at the end of the data' ); |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | return $n; |
| 132 | } |
| 133 | |
| 134 | protected function pull_no_more_than( $n ): int { |
| 135 | $this->buffer .= $this->internal_pull( self::CHUNK_SIZE_BYTES ); |
| 136 | |
| 137 | return min( $n, $this->count_consumable_bytes() ); |
| 138 | } |
| 139 | |
| 140 | public function consume_all(): string { |
| 141 | $body = ''; |
| 142 | while ( true ) { |
| 143 | if ( $this->reached_end_of_data() ) { |
| 144 | return $body; |
| 145 | } |
| 146 | $consumable = $this->pull( self::CHUNK_SIZE_BYTES ); |
| 147 | $body .= $this->consume( $consumable ); |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | protected function count_consumable_bytes(): int { |
| 152 | return strlen( $this->buffer ) - $this->offset_in_current_buffer; |
| 153 | } |
| 154 | |
| 155 | abstract protected function internal_pull( $n ): string; |
| 156 | |
| 157 | public function peek( int $n ): string { |
| 158 | return substr( $this->buffer, $this->offset_in_current_buffer, $n ); |
| 159 | } |
| 160 | |
| 161 | public function consume( int $n ): string { |
| 162 | if ( strlen( $this->buffer ) < $this->offset_in_current_buffer + $n ) { |
| 163 | throw new NotEnoughDataException( 'Cannot consume more bytes than available in the buffer.' ); |
| 164 | } |
| 165 | $bytes = substr( $this->buffer, $this->offset_in_current_buffer, $n ); |
| 166 | $this->offset_in_current_buffer += $n; |
| 167 | if ( $this->offset_in_current_buffer > $this->max_lookbehind_bytes ) { |
| 168 | $overflow = $this->offset_in_current_buffer - $this->max_lookbehind_bytes; |
| 169 | $this->offset_in_current_buffer -= $overflow; |
| 170 | $this->bytes_already_forgotten += $overflow; |
| 171 | $this->buffer = substr( $this->buffer, $overflow ); |
| 172 | } |
| 173 | |
| 174 | return $bytes; |
| 175 | } |
| 176 | |
| 177 | public function seek( int $target_offset ): void { |
| 178 | // We have that offset in the buffer, let's just update the pointer. |
| 179 | if ( $target_offset >= $this->bytes_already_forgotten && $target_offset <= $this->bytes_already_forgotten + strlen( $this->buffer ) ) { |
| 180 | $this->offset_in_current_buffer = $target_offset - $this->bytes_already_forgotten; |
| 181 | |
| 182 | return; |
| 183 | } |
| 184 | if ( null !== $this->length() && $target_offset > $this->length() ) { |
| 185 | $length = $this->length(); |
| 186 | throw new NotEnoughDataException( |
| 187 | esc_html( |
| 188 | sprintf( |
| 189 | 'Cannot seek to past the stream length (seeked to %d, stream length is %d).', |
| 190 | $target_offset, |
| 191 | $length |
| 192 | ) |
| 193 | ) |
| 194 | ); |
| 195 | } |
| 196 | |
| 197 | if ( $target_offset < 0 ) { |
| 198 | throw new ByteStreamException( 'Cannot seek to a negative offset' ); |
| 199 | } |
| 200 | |
| 201 | // Seeking outside of buffer range, we need a producer-specific implementation. |
| 202 | $this->seek_outside_of_buffer( $target_offset ); |
| 203 | } |
| 204 | |
| 205 | protected function seek_outside_of_buffer( int $target_offset ): void { |
| 206 | throw new ByteStreamException( 'Cannot seek outside of the buffered range' ); |
| 207 | } |
| 208 | |
| 209 | public function tell(): int { |
| 210 | return $this->bytes_already_forgotten + $this->offset_in_current_buffer; |
| 211 | } |
| 212 | |
| 213 | public function reached_end_of_data(): bool { |
| 214 | if ( $this->is_read_closed ) { |
| 215 | return true; |
| 216 | } |
| 217 | if ( $this->count_consumable_bytes() > 0 ) { |
| 218 | return false; |
| 219 | } |
| 220 | if ( null !== $this->length() ) { |
| 221 | return $this->tell() >= $this->length(); |
| 222 | } |
| 223 | |
| 224 | return $this->internal_reached_end_of_data(); |
| 225 | } |
| 226 | |
| 227 | protected function internal_reached_end_of_data(): bool { |
| 228 | return false; |
| 229 | } |
| 230 | |
| 231 | public function close_reading(): void { |
| 232 | $this->is_read_closed = true; |
| 233 | $this->internal_close_reading(); |
| 234 | } |
| 235 | |
| 236 | protected function internal_close_reading(): void { |
| 237 | } |
| 238 | } |
| 239 |