Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Exception/OutOfBoundsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php declare(strict_types=1);

namespace PrinsFrank\PdfParser\Exception;

class OutOfBoundsException extends PdfParserException {}
2 changes: 1 addition & 1 deletion src/Stream/FileStream.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
use PrinsFrank\PdfParser\Exception\InvalidArgumentException;
use PrinsFrank\PdfParser\Exception\RuntimeException;

class FileStream extends AbstractStream {
class FileStream extends AbstractStream implements PrimaryStream {
/** @var resource */
private readonly mixed $handle;

Expand Down
2 changes: 1 addition & 1 deletion src/Stream/InMemoryStream.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use PrinsFrank\PdfParser\Document\Generic\Marker;
use PrinsFrank\PdfParser\Exception\InvalidArgumentException;

class InMemoryStream extends AbstractStream {
class InMemoryStream extends AbstractStream implements PrimaryStream {
public function __construct(
private readonly string $content,
) {}
Expand Down
91 changes: 91 additions & 0 deletions src/Stream/Meta/BoundedStream.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php declare(strict_types=1);

namespace PrinsFrank\PdfParser\Stream\Meta;

use Override;
use PrinsFrank\PdfParser\Document\CMap\ToUnicode\ToUnicodeCMapOperator;
use PrinsFrank\PdfParser\Document\Generic\Character\DelimiterCharacter;
use PrinsFrank\PdfParser\Document\Generic\Character\WhitespaceCharacter;
use PrinsFrank\PdfParser\Document\Generic\Marker;
use PrinsFrank\PdfParser\Exception\InvalidArgumentException;
use PrinsFrank\PdfParser\Exception\NotImplementedException;
use PrinsFrank\PdfParser\Exception\OutOfBoundsException;
use PrinsFrank\PdfParser\Stream\AbstractStream;
use PrinsFrank\PdfParser\Stream\PrimaryStream;

class BoundedStream extends AbstractStream implements DerivedStream {
/** @throws InvalidArgumentException|OutOfBoundsException */
public function __construct(
private readonly PrimaryStream $primaryStream,
private readonly int $offsetStart,
private readonly int $offsetEnd,
) {
if ($this->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();
}
}
7 changes: 7 additions & 0 deletions src/Stream/Meta/DerivedStream.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php declare(strict_types=1);

namespace PrinsFrank\PdfParser\Stream\Meta;

use PrinsFrank\PdfParser\Stream\Stream;

interface DerivedStream extends Stream {}
5 changes: 5 additions & 0 deletions src/Stream/PrimaryStream.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php declare(strict_types=1);

namespace PrinsFrank\PdfParser\Stream;

interface PrimaryStream extends Stream {}
100 changes: 100 additions & 0 deletions tests/Unit/Stream/Meta/BoundedStreamTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php declare(strict_types=1);

namespace PrinsFrank\PdfParser\Tests\Unit\Stream\Meta;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use PrinsFrank\PdfParser\Exception\InvalidArgumentException;
use PrinsFrank\PdfParser\Exception\OutOfBoundsException;
use PrinsFrank\PdfParser\Stream\InMemoryStream;
use PrinsFrank\PdfParser\Stream\Meta\BoundedStream;
use PrinsFrank\PdfParser\Stream\PrimaryStream;

#[CoversClass(BoundedStream::class)]
class BoundedStreamTest extends TestCase {
public function testConstructThrowsExceptionOnInvalidBound(): void {
$this->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),
);
}
}
Loading