1<?php 2declare(strict_types=1); 3 4namespace PrinsFrank\PdfParser\Document\Dictionary; 5 6use PrinsFrank\PdfParser\Document\Dictionary\DictionaryParseContext\DictionaryParseContext; 7use PrinsFrank\PdfParser\Document\Dictionary\DictionaryParseContext\NestingContext; 8use PrinsFrank\PdfParser\Document\Generic\Character\DelimiterCharacter; 9use PrinsFrank\PdfParser\Document\Generic\Character\LiteralStringEscapeCharacter; 10use PrinsFrank\PdfParser\Document\Generic\Character\WhitespaceCharacter; 11use PrinsFrank\PdfParser\Document\Generic\Parsing\RollingCharBuffer; 12use PrinsFrank\PdfParser\Document\Security\EncryptionContext; 13use PrinsFrank\PdfParser\Exception\PdfParserException; 14use PrinsFrank\PdfParser\Stream\Stream; 15 16/** @internal */ 17class DictionaryParser { 18 /** 19 * @phpstan-assert int<0, max> $startPos 20 * @phpstan-assert int<1, max> $nrOfBytes 21 * 22 * @throws PdfParserException 23 */ 24 public static function parse(?EncryptionContext $encryptionContext, Stream $stream, int $startPos, int $nrOfBytes): Dictionary { 25 $dictionaryArray = []; 26 $rollingCharBuffer = new RollingCharBuffer(6); 27 $nestingContext = (new NestingContext())->setContext(DictionaryParseContext::ROOT); 28 $arrayNestingLevel = 0; 29 $contextBeforeComment = $previousIndexLevelDecrease = $previousIndexLevelIncrease = null; 30 foreach ($stream->chars($startPos, $nrOfBytes) as $index => $char) { 31 $rollingCharBuffer->next($char); 32 if ($char === DelimiterCharacter::LESS_THAN_SIGN->value 33 && $rollingCharBuffer->getPreviousCharacter() === DelimiterCharacter::LESS_THAN_SIGN->value 34 && $rollingCharBuffer->getPreviousCharacter(2) !== LiteralStringEscapeCharacter::REVERSE_SOLIDUS->value 35 && $nestingContext->getContext() !== DictionaryParseContext::VALUE_IN_SQUARE_BRACKETS 36 && $previousIndexLevelIncrease !== $index - 1) { 37 if ($nestingContext->getContext() === DictionaryParseContext::KEY) { 38 $nestingContext->removeFromKeyBuffer(); 39 } 40 41 $previousIndexLevelIncrease = $index; 42 $nestingContext->setContext(DictionaryParseContext::DICTIONARY)->incrementNesting()->setContext(DictionaryParseContext::DICTIONARY); 43 } elseif ($char === DelimiterCharacter::LESS_THAN_SIGN->value && $nestingContext->getContext() === DictionaryParseContext::KEY) { 44 $nestingContext->setContext(DictionaryParseContext::VALUE); 45 } elseif ($char === DelimiterCharacter::GREATER_THAN_SIGN->value 46 && $rollingCharBuffer->getPreviousCharacter() === DelimiterCharacter::GREATER_THAN_SIGN->value 47 && $rollingCharBuffer->getPreviousCharacter(2) !== LiteralStringEscapeCharacter::REVERSE_SOLIDUS->value 48 && $nestingContext->getContext() !== DictionaryParseContext::VALUE_IN_SQUARE_BRACKETS 49 && $previousIndexLevelDecrease !== $index - 1) { 50 $nestingContext->removeFromValueBuffer(); 51 self::flush($dictionaryArray, $nestingContext); 52 $previousIndexLevelDecrease = $index; 53 $nestingContext->decrementNesting()->flush(); 54 } elseif ($char === DelimiterCharacter::SOLIDUS->value 55 && $rollingCharBuffer->getPreviousCharacter() !== LiteralStringEscapeCharacter::REVERSE_SOLIDUS->value 56 && $nestingContext->getContext() !== DictionaryParseContext::VALUE_IN_SQUARE_BRACKETS) { 57 if ($nestingContext->getContext() === DictionaryParseContext::DICTIONARY) { 58 $nestingContext->setContext(DictionaryParseContext::KEY); 59 } elseif ($nestingContext->getContext() === DictionaryParseContext::VALUE) { 60 self::flush($dictionaryArray, $nestingContext); 61 $nestingContext->setContext(DictionaryParseContext::KEY); 62 } elseif ($nestingContext->getContext() === DictionaryParseContext::KEY || $nestingContext->getContext() === DictionaryParseContext::KEY_VALUE_SEPARATOR) { 63 $nestingContext->setContext(DictionaryParseContext::VALUE); 64 } 65 } elseif ($char === WhitespaceCharacter::LINE_FEED->value && $nestingContext->getContext() !== DictionaryParseContext::VALUE_IN_SQUARE_BRACKETS) { 66 if ($nestingContext->getContext() === DictionaryParseContext::KEY) { 67 $nestingContext->setContext(DictionaryParseContext::KEY_VALUE_SEPARATOR); 68 } elseif ($nestingContext->getContext() === DictionaryParseContext::VALUE) { 69 self::flush($dictionaryArray, $nestingContext); 70 } elseif ($nestingContext->getContext() === DictionaryParseContext::COMMENT) { 71 $nestingContext->setContext($contextBeforeComment ?? DictionaryParseContext::DICTIONARY); 72 $contextBeforeComment = null; 73 } 74 } elseif ($char === DelimiterCharacter::PERCENT_SIGN->value && $rollingCharBuffer->getPreviousCharacter() !== LiteralStringEscapeCharacter::REVERSE_SOLIDUS->value && $nestingContext->getContext() !== DictionaryParseContext::VALUE_IN_PARENTHESES) { 75 if ($nestingContext->getContext() === DictionaryParseContext::VALUE) { 76 self::flush($dictionaryArray, $nestingContext); 77 $contextBeforeComment = DictionaryParseContext::DICTIONARY; 78 } else { 79 $contextBeforeComment = $nestingContext->getContext(); 80 } 81 $nestingContext->setContext(DictionaryParseContext::COMMENT); 82 } elseif (WhitespaceCharacter::tryFrom($char) !== null && $nestingContext->getContext() === DictionaryParseContext::KEY) { 83 $nestingContext->setContext(DictionaryParseContext::KEY_VALUE_SEPARATOR); 84 } elseif ($char === DelimiterCharacter::LEFT_PARENTHESIS->value && (in_array($nestingContext->getContext(), [DictionaryParseContext::KEY, DictionaryParseContext::KEY_VALUE_SEPARATOR, DictionaryParseContext::VALUE], true))) { 85 $nestingContext->setContext(DictionaryParseContext::VALUE_IN_PARENTHESES); 86 } elseif ($char === DelimiterCharacter::RIGHT_PARENTHESIS->value && $rollingCharBuffer->getPreviousCharacter() !== LiteralStringEscapeCharacter::REVERSE_SOLIDUS->value && $nestingContext->getContext() === DictionaryParseContext::VALUE_IN_PARENTHESES) { 87 $nestingContext->setContext(DictionaryParseContext::VALUE); 88 } elseif ($char === DelimiterCharacter::LEFT_SQUARE_BRACKET->value && (in_array($nestingContext->getContext(), [DictionaryParseContext::KEY, DictionaryParseContext::KEY_VALUE_SEPARATOR, DictionaryParseContext::VALUE, DictionaryParseContext::VALUE_IN_SQUARE_BRACKETS], true))) { 89 $nestingContext->setContext(DictionaryParseContext::VALUE_IN_SQUARE_BRACKETS); 90 $arrayNestingLevel++; 91 } elseif ($char === DelimiterCharacter::RIGHT_SQUARE_BRACKET->value && $nestingContext->getContext() === DictionaryParseContext::VALUE_IN_SQUARE_BRACKETS) { 92 $arrayNestingLevel--; 93 if ($arrayNestingLevel === 0) { 94 $nestingContext->setContext(DictionaryParseContext::VALUE); 95 } 96 } elseif (trim($char) !== '' && $nestingContext->getContext() === DictionaryParseContext::KEY_VALUE_SEPARATOR) { 97 $nestingContext->setContext(DictionaryParseContext::VALUE); 98 } 99 100 match ($nestingContext->getContext()) { 101 DictionaryParseContext::KEY => $nestingContext->addToKeyBuffer($char), 102 DictionaryParseContext::VALUE_IN_PARENTHESES, 103 DictionaryParseContext::VALUE_IN_SQUARE_BRACKETS, 104 DictionaryParseContext::VALUE => $nestingContext->addToValueBuffer($char), 105 default => null, 106 }; 107 } 108 109 return DictionaryFactory::fromArray($encryptionContext, $dictionaryArray); 110 } 111 112 /** @param array<string, mixed> $dictionaryArray */ 113 private static function flush(array &$dictionaryArray, NestingContext $nestingContext): void { 114 if ($nestingContext->getValueBuffer()->isEmpty() || $nestingContext->getKeyBuffer()->isEmpty()) { 115 return; 116 } 117 118 $dictionaryArrayPointer = &$dictionaryArray; 119 $keys = $nestingContext->getKeysFromRoot(); 120 foreach ($keys as $index => $key) { 121 if ($key === (string) $nestingContext->getKeyBuffer() && $index === array_key_last($keys)) { 122 break; 123 } 124 125 /** @phpstan-ignore offsetAccess.nonOffsetAccessible */ 126 $dictionaryArrayPointer = &$dictionaryArrayPointer[trim($key)]; 127 } 128 129 /** @phpstan-ignore offsetAccess.nonOffsetAccessible */ 130 $dictionaryArrayPointer[(string) $nestingContext->getKeyBuffer()] = trim((string) $nestingContext->getValueBuffer()); 131 $nestingContext->flush(); 132 } 133} 134