diff --git a/src/Exception/OutOfBoundsException.php b/src/Exception/OutOfBoundsException.php new file mode 100644 index 00000000..7f745b23 --- /dev/null +++ b/src/Exception/OutOfBoundsException.php @@ -0,0 +1,5 @@ +offsetEnd < $this->offsetStart) { + throw new InvalidArgumentException('OffsetEnd should be bigger than offsetStart'); + } + + if ($this->offsetStart > $this->primaryStream->getSizeInBytes()) { + throw new OutOfBoundsException('Start of bounded stream should be within parent stream length'); + } + + if ($this->offsetEnd > $this->primaryStream->getSizeInBytes()) { + throw new OutOfBoundsException('End of bounded stream should be within parent stream length'); + } + } + + #[Override] + public function getSizeInBytes(): int { + return $this->offsetEnd - $this->offsetStart; + } + + /** @throws OutOfBoundsException */ + #[Override] + public function read(int $from, int $nrOfBytes): string { + if ($from + $nrOfBytes > $this->getSizeInBytes()) { + throw new OutOfBoundsException(sprintf('Stream is only %d bytes long, trying to read %d bytes from offset %d', $this->getSizeInBytes(), $nrOfBytes, $from)); + } + + return $this->primaryStream + ->read($this->offsetStart + $from, $nrOfBytes); + } + + #[Override] + public function slice(int $startByteOffset, int $endByteOffset): string { + if ($startByteOffset > $this->getSizeInBytes() || $endByteOffset > $this->getSizeInBytes()) { + throw new OutOfBoundsException(); + } + + return $this->primaryStream + ->read($this->offsetStart + $startByteOffset, $endByteOffset - $startByteOffset); + } + + #[Override] + public function chars(int $from, int $nrOfBytes): iterable { + if ($from + $nrOfBytes > $this->getSizeInBytes()) { + throw new OutOfBoundsException(); + } + + return $this->primaryStream + ->chars($this->offsetStart + $from, $nrOfBytes); + } + + #[Override] + public function firstPos(WhitespaceCharacter|DelimiterCharacter|ToUnicodeCMapOperator|Marker $needle, int $offsetFromStart, int $before): ?int { + if ($offsetFromStart > $this->getSizeInBytes() || $before > $this->getSizeInBytes()) { + throw new OutOfBoundsException(); + } + + $firstPos = $this->primaryStream + ->firstPos($needle, $this->offsetStart + $offsetFromStart, $this->offsetStart + $before); + if ($firstPos === null) { + return null; + } + + return $firstPos - $this->offsetStart; + } + + #[Override] + public function lastPos(WhitespaceCharacter|DelimiterCharacter|ToUnicodeCMapOperator|Marker $needle, int $offsetFromEnd): ?int { + throw new NotImplementedException(); + } +} diff --git a/src/Stream/Meta/DerivedStream.php b/src/Stream/Meta/DerivedStream.php new file mode 100644 index 00000000..e6698a73 --- /dev/null +++ b/src/Stream/Meta/DerivedStream.php @@ -0,0 +1,7 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('OffsetEnd should be bigger than offsetStart'); + new BoundedStream($this->createMock(PrimaryStream::class), 10, 9); + } + + public function testConstructThrowsExceptionWhenStartBiggerThanEndOfPrimaryStream(): void { + $primaryStream = $this->createMock(PrimaryStream::class); + $primaryStream->expects(self::once()) + ->method('getSizeInBytes') + ->willReturn(42); + + $this->expectException(OutOfBoundsException::class); + $this->expectExceptionMessage('Start of bounded stream should be within parent stream length'); + new BoundedStream($primaryStream, 43, 44); + } + + public function testConstructThrowsExceptionWhenEndBiggerThanEndOfPrimaryStream(): void { + $primaryStream = $this->createMock(PrimaryStream::class); + $primaryStream->expects(self::atLeastOnce()) + ->method('getSizeInBytes') + ->willReturn(43); + + $this->expectException(OutOfBoundsException::class); + $this->expectExceptionMessage('End of bounded stream should be within parent stream length'); + new BoundedStream($primaryStream, 43, 44); + } + + public function testGetSizeInBytes(): void { + $primaryStream = $this->createMock(PrimaryStream::class); + $primaryStream->expects(self::atLeastOnce()) + ->method('getSizeInBytes') + ->willReturn(44); + + static::assertSame(0, (new BoundedStream($primaryStream, 42, 42))->getSizeInBytes()); + static::assertSame(1, (new BoundedStream($primaryStream, 42, 43))->getSizeInBytes()); + static::assertSame(2, (new BoundedStream($primaryStream, 42, 44))->getSizeInBytes()); + } + + public function testReadThrowsOutOfBoundsException(): void { + $primaryStream = $this->createMock(PrimaryStream::class); + $primaryStream->expects(self::atLeastOnce()) + ->method('getSizeInBytes') + ->willReturn(30); + + $this->expectException(OutOfBoundsException::class); + $this->expectExceptionMessage('Stream is only 10 bytes long, trying to read 15 bytes from offset 5'); + (new BoundedStream($primaryStream, 10, 20)) + ->read(5, 15); + } + + public function testRead(): void { + static::assertSame( + '012', + (new BoundedStream( + new InMemoryStream('0123456789'), + 0, + 10, + ))->read(0, 3), + ); + static::assertSame( + '123', + (new BoundedStream( + new InMemoryStream('0123456789'), + 1, + 10, + ))->read(0, 3), + ); + static::assertSame( + '234', + (new BoundedStream( + new InMemoryStream('0123456789'), + 1, + 10, + ))->read(1, 3), + ); + static::assertSame( + '678', + (new BoundedStream( + new InMemoryStream('0123456789'), + 3, + 10, + ))->read(3, 3), + ); + } +}