Repository: PrinsFrank/pdfparser (php)

Issue:
Nested Array/Dictionary/DictionaryArrays are not parsed with their proper nesting
Related to #48
See this example:
```
/OCProperties <<
    /D <<
        /AS [
            <<
                /Category [/Print]
                /Event/Print
                /OCGs [939 0 R 419 0 R]
            >>
            <<
                /Category [/View]
                /Event/View
                /OCGs[939 0 R 419 0 R]
            >>
        ]
        /BaseState/OFF
        /ON[419 0 R]
        /Order[]
        /RBGroups[]
    >>
    /OCGs[939 0 R 419 0 R]
>>
```

The 5th row already closes one level of nesting too early, as the closing square bracket for the [/Print] section also closes the /AS DictionaryArray

Pull request:
Fix issue where nested arrays in dictionaries were closed too early
Fixes #55 

Patch:
diff --git a/src/Document/Dictionary/DictionaryParser.php b/src/Document/Dictionary/DictionaryParser.php
index fa0a27a..895063c 100644
--- a/src/Document/Dictionary/DictionaryParser.php
+++ b/src/Document/Dictionary/DictionaryParser.php
@@ -24,6 +24,7 @@ class DictionaryParser {
         $dictionaryArray = [];
         $rollingCharBuffer = new RollingCharBuffer(6);
         $nestingContext = (new NestingContext())->setContext(DictionaryParseContext::ROOT);
+        $arrayNestingLevel = 0;
         foreach ($stream->chars($startPos, $nrOfBytes) as $char) {
             $rollingCharBuffer->next($char);
             if ($char === DelimiterCharacter::LESS_THAN_SIGN->value && $rollingCharBuffer->getPreviousCharacter() === DelimiterCharacter::LESS_THAN_SIGN->value && $rollingCharBuffer->getPreviousCharacter(2) !== LiteralStringEscapeCharacter::REVERSE_SOLIDUS->value && $nestingContext->getContext() !== DictionaryParseContext::VALUE_IN_SQUARE_BRACKETS) {
@@ -38,7 +39,7 @@ class DictionaryParser {
                 $nestingContext->removeFromValueBuffer();
                 self::flush($dictionaryArray, $nestingContext);
                 $nestingContext->decrementNesting()->flush();
-            } elseif ($char === DelimiterCharacter::SOLIDUS->value && $rollingCharBuffer->getPreviousCharacter() !== LiteralStringEscapeCharacter::REVERSE_SOLIDUS->value) {
+            } elseif ($char === DelimiterCharacter::SOLIDUS->value && $rollingCharBuffer->getPreviousCharacter() !== LiteralStringEscapeCharacter::REVERSE_SOLIDUS->value && $nestingContext->getContext() !== DictionaryParseContext::VALUE_IN_SQUARE_BRACKETS) {
                 if ($nestingContext->getContext() === DictionaryParseContext::DICTIONARY) {
                     $nestingContext->setContext(DictionaryParseContext::KEY);
                 } elseif ($nestingContext->getContext() === DictionaryParseContext::VALUE) {
@@ -47,7 +48,7 @@ class DictionaryParser {
                 } elseif ($nestingContext->getContext() === DictionaryParseContext::KEY || $nestingContext->getContext() === DictionaryParseContext::KEY_VALUE_SEPARATOR) {
                     $nestingContext->setContext(DictionaryParseContext::VALUE);
                 }
-            } elseif ($char === LiteralStringEscapeCharacter::LINE_FEED->value) {
+            } elseif ($char === LiteralStringEscapeCharacter::LINE_FEED->value && $nestingContext->getContext() !== DictionaryParseContext::VALUE_IN_SQUARE_BRACKETS) {
                 if ($nestingContext->getContext() === DictionaryParseContext::KEY) {
                     $nestingContext->setContext(DictionaryParseContext::VALUE);
                 } elseif ($nestingContext->getContext() === DictionaryParseContext::VALUE) {
@@ -61,10 +62,14 @@ class DictionaryParser {
                 $nestingContext->setContext(DictionaryParseContext::VALUE_IN_PARENTHESES);
             } elseif ($char === DelimiterCharacter::RIGHT_PARENTHESIS->value && $rollingCharBuffer->getPreviousCharacter() !== LiteralStringEscapeCharacter::REVERSE_SOLIDUS->value && $nestingContext->getContext() === DictionaryParseContext::VALUE_IN_PARENTHESES) {
                 $nestingContext->setContext(DictionaryParseContext::VALUE);
-            } elseif ($char === DelimiterCharacter::LEFT_SQUARE_BRACKET->value && (in_array($nestingContext->getContext(), [DictionaryParseContext::KEY, DictionaryParseContext::KEY_VALUE_SEPARATOR, DictionaryParseContext::VALUE], true))) {
+            } 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))) {
                 $nestingContext->setContext(DictionaryParseContext::VALUE_IN_SQUARE_BRACKETS);
+                $arrayNestingLevel++;
             } elseif ($char === DelimiterCharacter::RIGHT_SQUARE_BRACKET->value && $nestingContext->getContext() === DictionaryParseContext::VALUE_IN_SQUARE_BRACKETS) {
-                $nestingContext->setContext(DictionaryParseContext::VALUE);
+                $arrayNestingLevel--;
+                if ($arrayNestingLevel === 0) {
+                    $nestingContext->setContext(DictionaryParseContext::VALUE);
+                }
             } elseif (trim($char) !== '' && $nestingContext->getContext() === DictionaryParseContext::KEY_VALUE_SEPARATOR) {
                 $nestingContext->setContext(DictionaryParseContext::VALUE);
             } elseif ($char === DelimiterCharacter::PERCENT_SIGN->value && $rollingCharBuffer->getPreviousCharacter() !== LiteralStringEscapeCharacter::REVERSE_SOLIDUS->value) {
diff --git a/src/Document/Dictionary/DictionaryValue/Array/DictionaryArrayValue.php b/src/Document/Dictionary/DictionaryValue/Array/DictionaryArrayValue.php
index b1639ef..5a6d68d 100644
--- a/src/Document/Dictionary/DictionaryValue/Array/DictionaryArrayValue.php
+++ b/src/Document/Dictionary/DictionaryValue/Array/DictionaryArrayValue.php
@@ -11,16 +11,20 @@ use PrinsFrank\PdfParser\Exception\RuntimeException;
 use PrinsFrank\PdfParser\Stream\InMemoryStream;
 
 class DictionaryArrayValue implements DictionaryValue {
-    /** @param list<Dictionary> $dictionaries */
+    /** @var list<Dictionary> */
+    public readonly array $dictionaries;
+
+    /** @no-named-arguments */
     public function __construct(
-        public readonly array $dictionaries,
+        Dictionary... $dictionaries,
     ) {
+        $this->dictionaries = $dictionaries;
     }
 
     #[Override]
     /** @throws PdfParserException */
     public static function fromValue(string $valueString): ?self {
-        $valueStringWithoutSpaces = str_replace(' ', '', $valueString);
+        $valueStringWithoutSpaces = str_replace([' ', "\r", "\n"], '', $valueString);
         if (!str_starts_with($valueStringWithoutSpaces, '[<<') || !str_ends_with($valueStringWithoutSpaces, '>>]')) {
             return null;
         }
@@ -34,6 +38,6 @@ class DictionaryArrayValue implements DictionaryValue {
                 : DictionaryParser::parse($memoryStream = new InMemoryStream('<<' . $dictionaryValueString . '>>'), 0, $memoryStream->getSizeInBytes());
         }
 
-        return new self($dictionaryEntries);
+        return new self(... $dictionaryEntries);
     }
 }


Test patch:
diff --git a/tests/Unit/Document/Dictionary/DictionaryParserTest.php b/tests/Unit/Document/Dictionary/DictionaryParserTest.php
index e7f8027..070095d 100644
--- a/tests/Unit/Document/Dictionary/DictionaryParserTest.php
+++ b/tests/Unit/Document/Dictionary/DictionaryParserTest.php
@@ -12,9 +12,11 @@ use PrinsFrank\PdfParser\Document\Dictionary\DictionaryKey\DictionaryKey;
 use PrinsFrank\PdfParser\Document\Dictionary\DictionaryKey\ExtendedDictionaryKey;
 use PrinsFrank\PdfParser\Document\Dictionary\DictionaryParser;
 use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\Array\ArrayValue;
+use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\Array\DictionaryArrayValue;
 use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\Array\WValue;
 use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\Date\DateValue;
 use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\Integer\IntegerValue;
+use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\Name\EventNameValue;
 use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\Name\FilterNameValue;
 use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\Name\PageModeNameValue;
 use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\Name\TabsNameValue;
@@ -22,6 +24,7 @@ use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\Name\TrappedNameVal
 use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\Name\TypeNameValue;
 use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\Rectangle\Rectangle;
 use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\Reference\ReferenceValue;
+use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\Reference\ReferenceValueArray;
 use PrinsFrank\PdfParser\Document\Dictionary\DictionaryValue\TextString\TextStringValue;
 use PrinsFrank\PdfParser\Stream\InMemoryStream;
 
@@ -291,4 +294,96 @@ class DictionaryParserTest extends TestCase {
             DictionaryParser::parse($stream, 0, $stream->getSizeInBytes())
         );
     }
+
+    public function testHandlesNestedArrays(): void {
+        $stream = new InMemoryStream(
+            <<<EOD
+                <<
+                    /OCProperties <<
+                        /D <<
+                            /AS [
+                                <<
+                                    /Category [/Print]
+                                    /Event/Print
+                                    /OCGs [939 0 R 419 0 R]
+                                >>
+                                <<
+                                    /Category [/View]
+                                    /Event/View
+                                    /OCGs[939 0 R 419 0 R]
+                                >>
+                            ]
+                            /BaseState/OFF
+                            /ON[419 0 R]
+                            /Order[]
+                            /RBGroups[]
+                        >>
+                        /OCGs[939 0 R 419 0 R]
+                    >>
+                >>
+            EOD
+        );
+        static::assertEquals(
+            new Dictionary(
+                new DictionaryEntry(
+                    DictionaryKey::OCPROPERTIES,
+                    new Dictionary(
+                        new DictionaryEntry(
+                            DictionaryKey::D,
+                            new Dictionary(
+                                new DictionaryEntry(
+                                    DictionaryKey::AS,
+                                    new DictionaryArrayValue(
+                                        new Dictionary(
+                                            new DictionaryEntry(
+                                                DictionaryKey::CATEGORY,
+                                                new ArrayValue(['/View'])
+                                            ),
+                                            new DictionaryEntry(
+                                                DictionaryKey::EVENT,
+                                                EventNameValue::View
+                                            ),
+                                            new DictionaryEntry(
+                                                DictionaryKey::OCGS,
+                                                new ReferenceValueArray(
+                                                    new ReferenceValue(939, 0),
+                                                    new ReferenceValue(419, 0),
+                                                )
+                                            ),
+                                        ),
+                                    )
+                                ),
+                                new DictionaryEntry(
+                                    DictionaryKey::BASE_STATE,
+                                    new TextStringValue('/OFF')
+                                ),
+                                new DictionaryEntry(
+                                    DictionaryKey::ON,
+                                    new ReferenceValueArray(
+                                        new ReferenceValue(419, 0),
+                                    )
+                                ),
+                                new DictionaryEntry(
+                                    DictionaryKey::ORDER,
+                                    new ArrayValue([])
+                                ),
+                                new DictionaryEntry(
+                                    DictionaryKey::RBGROUPS,
+                                    new ArrayValue([])
+                                ),
+                            )
+                        ),
+                        new DictionaryEntry(
+                            DictionaryKey::OCGS,
+                            new ReferenceValueArray(
+                                new ReferenceValue(939, 0),
+                                new ReferenceValue(419, 0),
+                            )
+                        )
+                    )
+                )
+            ),
+            DictionaryParser::parse($stream, 0, $stream->getSizeInBytes()),
+        );
+    }
 }
diff --git a/tests/Unit/Document/Dictionary/DictionaryValue/Array/DictionaryArrayValueTest.php b/tests/Unit/Document/Dictionary/DictionaryValue/Array/DictionaryArrayValueTest.php
index d2bc4d6..311b07e 100644
--- a/tests/Unit/Document/Dictionary/DictionaryValue/Array/DictionaryArrayValueTest.php
+++ b/tests/Unit/Document/Dictionary/DictionaryValue/Array/DictionaryArrayValueTest.php
@@ -20,33 +20,31 @@ class DictionaryArrayValueTest extends TestCase {
         static::assertNull(DictionaryArrayValue::fromValue(''));
         static::assertNull(DictionaryArrayValue::fromValue('[]'));
         static::assertEquals(
-            new DictionaryArrayValue([new Dictionary()]),
+            new DictionaryArrayValue(new Dictionary()),
             DictionaryArrayValue::fromValue('[<<>>]')
         );
         static::assertEquals(
-            new DictionaryArrayValue([new Dictionary()]),
+            new DictionaryArrayValue(new Dictionary()),
             DictionaryArrayValue::fromValue('[ <<>> ]')
         );
         static::assertEquals(
-            new DictionaryArrayValue([new Dictionary(), new Dictionary()]),
+            new DictionaryArrayValue(new Dictionary(), new Dictionary()),
             DictionaryArrayValue::fromValue('[<<>> <<>>]')
         );
         static::assertEquals(
-            new DictionaryArrayValue([new Dictionary(new DictionaryEntry(DictionaryKey::LENGTH, new IntegerValue(106))), new Dictionary(new DictionaryEntry(DictionaryKey::TITLE, new TextStringValue('(Foo)')))]),
+            new DictionaryArrayValue(new Dictionary(new DictionaryEntry(DictionaryKey::LENGTH, new IntegerValue(106))), new Dictionary(new DictionaryEntry(DictionaryKey::TITLE, new TextStringValue('(Foo)')))),
             DictionaryArrayValue::fromValue('[<</Length 106>> <</Title(Foo)>>]')
         );
         static::assertEquals(
             new DictionaryArrayValue(
-                [
-                    new Dictionary(
-                        new DictionaryEntry(DictionaryKey::TYPE, TypeNameValue::OUTPUT_INTENT),
-                        new DictionaryEntry(DictionaryKey::S, new TextStringValue('/GTS_PDFA1')),
-                        new DictionaryEntry(new ExtendedDictionaryKey('OutputConditionIdentifier'), new TextStringValue('(sRGB)')),
-                        new DictionaryEntry(new ExtendedDictionaryKey('RegistryName'), new TextStringValue('(http://www.color.org)')),
-                        new DictionaryEntry(DictionaryKey::INFO, new TextStringValue('(Creator: HP     Manufacturer:IEC    Model:sRGB)')),
-                        new DictionaryEntry(new ExtendedDictionaryKey('DestOutputProfile'), new ReferenceValue(361, 0)),
-                    )
-                ]
+                new Dictionary(
+                    new DictionaryEntry(DictionaryKey::TYPE, TypeNameValue::OUTPUT_INTENT),
+                    new DictionaryEntry(DictionaryKey::S, new TextStringValue('/GTS_PDFA1')),
+                    new DictionaryEntry(new ExtendedDictionaryKey('OutputConditionIdentifier'), new TextStringValue('(sRGB)')),
+                    new DictionaryEntry(new ExtendedDictionaryKey('RegistryName'), new TextStringValue('(http://www.color.org)')),
+                    new DictionaryEntry(DictionaryKey::INFO, new TextStringValue('(Creator: HP     Manufacturer:IEC    Model:sRGB)')),
+                    new DictionaryEntry(new ExtendedDictionaryKey('DestOutputProfile'), new ReferenceValue(361, 0)),
+                )
             ),
             DictionaryArrayValue::fromValue('[<</Type/OutputIntent/S/GTS_PDFA1/OutputConditionIdentifier(sRGB) /RegistryName(http://www.color.org) /Info(Creator: HP     Manufacturer:IEC    Model:sRGB) /DestOutputProfile 361 0 R>>]')
         );