Skip to content
Merged
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
12 changes: 11 additions & 1 deletion src/Document/CrossReference/CrossReferenceSourceParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

namespace PrinsFrank\PdfParser\Document\CrossReference;

use PrinsFrank\PdfParser\Document\CrossReference\RawStream\ObjectPositionsFromRawStreamParser;
use PrinsFrank\PdfParser\Document\CrossReference\Source\CrossReferenceSource;
use PrinsFrank\PdfParser\Document\CrossReference\Source\RecoveredCrossReferenceSource;
use PrinsFrank\PdfParser\Document\CrossReference\Stream\CrossReferenceStreamParser;
use PrinsFrank\PdfParser\Document\CrossReference\Table\CrossReferenceTableParser;
use PrinsFrank\PdfParser\Document\Dictionary\DictionaryKey\DictionaryKey;
Expand Down Expand Up @@ -80,7 +82,15 @@ public static function parse(Stream $stream): CrossReferenceSource {
$crossReferenceSections[] = $currentCrossReferenceSection;
}

return new CrossReferenceSource(... $crossReferenceSections);
$crossReferenceSource = new CrossReferenceSource(... $crossReferenceSections);
if ($crossReferenceSource->hasInvalidByteOffset($stream)) {
return new RecoveredCrossReferenceSource(
ObjectPositionsFromRawStreamParser::parse($stream),
...$crossReferenceSections,
);
}

return $crossReferenceSource;
}

private static function getCrossReferenceType(Stream $stream, int $byteOffsetLastCrossReferenceSection, int $byteOffsetEndOfCurrentLine): ?CrossReferenceType {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php declare(strict_types=1);

namespace PrinsFrank\PdfParser\Document\CrossReference\RawStream;

use PrinsFrank\PdfParser\Stream\Stream;

class ObjectPositionsFromRawStreamParser {
/** @return array<int, int> */
public static function parse(Stream $stream): array {
$inObjNr = $inObjGenerationNumber = $pendingObjMarker = false;
$startObjNrOffset = $objNrBuffer = $objMarkerBuffer = null;
$discoveredObjects = [];
foreach ($stream->chars(0, $stream->getSizeInBytes()) as $byteOffset => $char) {
if ($char === ' ') {
if ($inObjNr === true) {
$inObjNr = false;
$inObjGenerationNumber = true;
} elseif ($inObjGenerationNumber === true) {
$inObjGenerationNumber = false;
$pendingObjMarker = true;
} else {
$inObjNr = $inObjGenerationNumber = $pendingObjMarker = false;
$startObjNrOffset = $objNrBuffer = $objMarkerBuffer = null;
}
} elseif ($char === '0'
|| $char === '1'
|| $char === '2'
|| $char === '3'
|| $char === '4'
|| $char === '5'
|| $char === '6'
|| $char === '7'
|| $char === '8'
|| $char === '9') {
if ($pendingObjMarker === true) {
$pendingObjMarker = false;
$objNrBuffer = null;
} elseif ($inObjGenerationNumber === true) {
} elseif ($inObjNr === false) {
$inObjNr = true;
$startObjNrOffset = $byteOffset;
$objNrBuffer = $char;
} elseif ($inObjNr === true) {
$objNrBuffer .= $char;
}
} elseif ($pendingObjMarker === true) {
if ($objMarkerBuffer === null && $char === 'o') { // @phpstan-ignore identical.alwaysTrue
$objMarkerBuffer = $char;
} elseif ($objMarkerBuffer === 'o' && $char === 'b') { // @phpstan-ignore identical.alwaysFalse, booleanAnd.alwaysFalse
$objMarkerBuffer .= $char;
} elseif ($objMarkerBuffer === 'ob' && $char === 'j') { // @phpstan-ignore identical.alwaysFalse, booleanAnd.alwaysFalse
$discoveredObjects[(int) $objNrBuffer] = $startObjNrOffset;
$inObjNr = $inObjGenerationNumber = $pendingObjMarker = false;
$startObjNrOffset = $objNrBuffer = $objMarkerBuffer = null;
} else {
$inObjNr = $inObjGenerationNumber = $pendingObjMarker = false;
$startObjNrOffset = $objNrBuffer = $objMarkerBuffer = null;
}
} else {
$inObjNr = $inObjGenerationNumber = $pendingObjMarker = false;
$startObjNrOffset = $objNrBuffer = $objMarkerBuffer = null;
}
}

return $discoveredObjects;
}
}
14 changes: 13 additions & 1 deletion src/Document/CrossReference/Source/CrossReferenceSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\DictionaryValue;
use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\Name\NameValue;
use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\Reference\ReferenceValue;
use PrinsFrank\PdfParser\Document\Document;
use PrinsFrank\PdfParser\Exception\ParseFailureException;
use PrinsFrank\PdfParser\Stream\Stream;

/** Can be both from a crossReferenceTable or a crossReferenceStream */
class CrossReferenceSource {
Expand All @@ -26,7 +28,7 @@ public function __construct(
$this->crossReferenceSections = $crossReferenceSections;
}

public function getCrossReferenceEntry(int $objNumber): CrossReferenceEntryInUseObject|CrossReferenceEntryCompressed|null {
public function getCrossReferenceEntry(int $objNumber, Document $document): CrossReferenceEntryInUseObject|CrossReferenceEntryCompressed|null {
foreach ($this->crossReferenceSections as $crossReferenceSection) {
$crossReferenceEntry = $crossReferenceSection->getCrossReferenceEntry($objNumber);
if ($crossReferenceEntry !== null) {
Expand Down Expand Up @@ -75,4 +77,14 @@ public function getFirstId(): string {

return $firstId;
}

public function hasInvalidByteOffset(Stream $stream): bool {
foreach ($this->crossReferenceSections as $crossReferenceSection) {
if ($crossReferenceSection->hasInvalidByteOffset($stream)) {
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php declare(strict_types=1);

namespace PrinsFrank\PdfParser\Document\CrossReference\Source;

use Override;
use PrinsFrank\PdfParser\Document\CrossReference\Source\Section\CrossReferenceSection;
use PrinsFrank\PdfParser\Document\CrossReference\Source\Section\SubSection\Entry\CrossReferenceEntryCompressed;
use PrinsFrank\PdfParser\Document\CrossReference\Source\Section\SubSection\Entry\CrossReferenceEntryInUseObject;
use PrinsFrank\PdfParser\Document\Document;

class RecoveredCrossReferenceSource extends CrossReferenceSource {
/**
* @param array<int, int> $recoveredByteOffsetMap where the key is the object nr and the value the byte offset
*
* @no-named-arguments
*/
public function __construct(
private readonly array $recoveredByteOffsetMap,
CrossReferenceSection... $crossReferenceSections,
) {
parent::__construct(...$crossReferenceSections);
}

#[Override]
public function getCrossReferenceEntry(int $objNumber, Document $document): CrossReferenceEntryInUseObject|CrossReferenceEntryCompressed|null {
$crossReferenceEntry = parent::getCrossReferenceEntry($objNumber, $document);
if ($crossReferenceEntry instanceof CrossReferenceEntryInUseObject
&& $document->stream->read($crossReferenceEntry->byteOffsetInDecodedStream, strlen($expectedStartObjMarker = sprintf('%d %d obj', $objNumber, $crossReferenceEntry->generationNumber))) === $expectedStartObjMarker) {
return $crossReferenceEntry;
}

if (array_key_exists($objNumber, $this->recoveredByteOffsetMap)) {
return new CrossReferenceEntryInUseObject($this->recoveredByteOffsetMap[$objNumber], 0);
}

return $crossReferenceEntry;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PrinsFrank\PdfParser\Document\CrossReference\Source\Section\SubSection\Entry\CrossReferenceEntryCompressed;
use PrinsFrank\PdfParser\Document\CrossReference\Source\Section\SubSection\Entry\CrossReferenceEntryInUseObject;
use PrinsFrank\PdfParser\Document\Dictionary\Dictionary;
use PrinsFrank\PdfParser\Stream\Stream;

/** There are multiple crossReference sections if there are incremental updates. See 7.5.6 */
readonly class CrossReferenceSection {
Expand All @@ -29,4 +30,14 @@ public function getCrossReferenceEntry(int $objNumber): CrossReferenceEntryInUse

return null;
}

public function hasInvalidByteOffset(Stream $stream): bool {
foreach ($this->crossReferenceSubSections as $crossReferenceSubSection) {
if ($crossReferenceSubSection->hasInvalidByteOffset($stream)) {
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PrinsFrank\PdfParser\Document\CrossReference\Source\Section\SubSection\Entry\CrossReferenceEntryInUseObject;
use PrinsFrank\PdfParser\Exception\InvalidArgumentException;
use PrinsFrank\PdfParser\Exception\RuntimeException;
use PrinsFrank\PdfParser\Stream\Stream;

readonly class CrossReferenceSubSection {
/** @var array<CrossReferenceEntryInUseObject|CrossReferenceEntryFreeObject|CrossReferenceEntryCompressed> */
Expand Down Expand Up @@ -51,4 +52,24 @@ public function getCrossReferenceEntry(int $objNumber): CrossReferenceEntryInUse

return $object;
}

public function hasInvalidByteOffset(Stream $stream): bool {
foreach ($this->crossReferenceEntries as $index => $crossReferenceEntry) {
if ($crossReferenceEntry instanceof CrossReferenceEntryInUseObject === false) {
continue;
}

if ($crossReferenceEntry->byteOffsetInDecodedStream > $stream->getSizeInBytes()) {
return true;
}

$objNumber = $this->firstObjectNumber + $index;
$expectedObjMarker = $objNumber . ' ' . $crossReferenceEntry->generationNumber . ' obj';
if ($stream->read($crossReferenceEntry->byteOffsetInDecodedStream, strlen($expectedObjMarker)) !== $expectedObjMarker) {
return true;
}
}

return false;
}
}
2 changes: 1 addition & 1 deletion src/Document/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public function getObject(int $objectNumber, ?string $expectedDecoratorFQN = nul
return $this->objectCache[$objectNumber];
}

$crossReferenceEntry = $this->crossReferenceSource->getCrossReferenceEntry($objectNumber);
$crossReferenceEntry = $this->crossReferenceSource->getCrossReferenceEntry($objectNumber, $this);
if ($crossReferenceEntry === null) {
return null;
}
Expand Down
21 changes: 17 additions & 4 deletions tests/Feature/H7Stage1UpdatingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\TestCase;
use PrinsFrank\PdfParser\Document\CrossReference\Source\CrossReferenceSource;
use PrinsFrank\PdfParser\Document\CrossReference\Source\RecoveredCrossReferenceSource;
use PrinsFrank\PdfParser\Document\CrossReference\Source\Section\CrossReferenceSection;
use PrinsFrank\PdfParser\Document\CrossReference\Source\Section\SubSection\CrossReferenceSubSection;
use PrinsFrank\PdfParser\Document\CrossReference\Source\Section\SubSection\Entry\CrossReferenceEntryFreeObject;
Expand Down Expand Up @@ -36,7 +36,20 @@ public function testStructure(): void {

static::assertSame(Version::V1_4, $document->version);
static::assertEquals(
new CrossReferenceSource(
new RecoveredCrossReferenceSource(
[
1 => 9,
2 => 74,
3 => 120,
4 => 604,
5 => 704,
6 => 788,
7 => 812,
8 => 856,
9 => 958,
10 => 1062,
11 => 1166,
],
new CrossReferenceSection(
new Dictionary(
new DictionaryEntry(DictionaryKey::SIZE, new IntegerValue(12)),
Expand Down Expand Up @@ -217,7 +230,7 @@ public function testStructure(): void {
$document,
7,
0,
811,
812,
855,
),
$document,
Expand Down Expand Up @@ -307,7 +320,7 @@ public function testStructure(): void {
$document,
11,
0,
1165,
1166,
1269,
),
$document,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use PrinsFrank\PdfParser\Document\CrossReference\CrossReferenceSourceParser;
use PrinsFrank\PdfParser\Document\CrossReference\Source\CrossReferenceSource;
use PrinsFrank\PdfParser\Document\CrossReference\Source\RecoveredCrossReferenceSource;
use PrinsFrank\PdfParser\Document\CrossReference\Source\Section\CrossReferenceSection;
use PrinsFrank\PdfParser\Document\CrossReference\Source\Section\SubSection\CrossReferenceSubSection;
use PrinsFrank\PdfParser\Document\CrossReference\Source\Section\SubSection\Entry\CrossReferenceEntryFreeObject;
Expand All @@ -21,7 +21,8 @@
class CrossReferenceSourceParserTest extends TestCase {
public function testParse(): void {
static::assertEquals(
new CrossReferenceSource(
new RecoveredCrossReferenceSource(
[],
new CrossReferenceSection(
new Dictionary(
new DictionaryEntry(DictionaryKey::SIZE, new IntegerValue(7)),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php declare(strict_types=1);

namespace PrinsFrank\PdfParser\Tests\Unit\Document\CrossReference\RawStream;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use PrinsFrank\PdfParser\Document\CrossReference\RawStream\ObjectPositionsFromRawStreamParser;
use PrinsFrank\PdfParser\Stream\InMemoryStream;

#[CoversClass(ObjectPositionsFromRawStreamParser::class)]
class ObjectPositionsFromRawStreamParserTest extends TestCase {
public function testParse(): void {
static::assertSame(
[
1 => 10,
1232131 => 42,
],
ObjectPositionsFromRawStreamParser::parse(
new InMemoryStream(
<<<PDF
%%PDF-1.7
1 0 obj
4 0 4 0 testobj
endobj

1232131 0 obj
endobj

PDF,
),
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use PrinsFrank\PdfParser\Document\Dictionary\DictionaryEntry\DictionaryEntry;
use PrinsFrank\PdfParser\Document\Dictionary\DictionaryKey\DictionaryKey;
use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\Reference\ReferenceValue;
use PrinsFrank\PdfParser\Document\Document;

#[CoversClass(CrossReferenceSource::class)]
class CrossReferenceSourceTest extends TestCase {
Expand All @@ -34,9 +35,9 @@ public function testGetCrossReferenceEntry(): void {
),
),
);
static::assertSame($crossReferenceEntry1, $crossReferenceSource->getCrossReferenceEntry(42));
static::assertSame($crossReferenceEntry2, $crossReferenceSource->getCrossReferenceEntry(43));
static::assertNull($crossReferenceSource->getCrossReferenceEntry(44));
static::assertSame($crossReferenceEntry1, $crossReferenceSource->getCrossReferenceEntry(42, $this->createMock(Document::class)));
static::assertSame($crossReferenceEntry2, $crossReferenceSource->getCrossReferenceEntry(43, $this->createMock(Document::class)));
static::assertNull($crossReferenceSource->getCrossReferenceEntry(44, $this->createMock(Document::class)));
}

public function testGetReferenceForKey(): void {
Expand Down
Loading