@@ -150,6 +150,7 @@ protected function parseTrailer(array $structure, ?Document $document)
150150 protected function parseObject (string $ id , array $ structure , ?Document $ document )
151151 {
152152 $ header = new Header ([], $ document );
153+ $ headerStructure = [];
153154 $ content = '' ;
154155
155156 foreach ($ structure as $ position => $ part ) {
@@ -170,28 +171,35 @@ protected function parseObject(string $id, array $structure, ?Document $document
170171 break ;
171172
172173 case '<< ' :
174+ $ headerStructure = $ part [1 ];
173175 $ header = $ this ->parseHeader ($ part [1 ], $ document );
174176 break ;
175177
176178 case 'stream ' :
177179 $ content = isset ($ part [3 ][0 ]) ? $ part [3 ][0 ] : $ part [1 ];
178180
179181 if ($ header ->get ('Type ' )->equals ('ObjStm ' )) {
180- $ numberOfObjects = $ this ->objectStreamInteger ($ header , 'N ' );
181- $ firstObjectOffset = $ this ->objectStreamInteger ($ header , 'First ' );
182+ $ numberOfObjects = $ this ->objectStreamInteger ($ headerStructure , 'N ' );
183+ $ firstObjectOffset = $ this ->objectStreamInteger ($ headerStructure , 'First ' );
184+
185+ if (null === $ numberOfObjects || null === $ firstObjectOffset ) {
186+ list ($ xrefs , $ firstObjectOffset ) = $ this ->parseObjectStreamIndexWithoutMetadata ($ content );
187+ $ numberOfObjects = \count ($ xrefs );
188+ } else {
189+ if ($ firstObjectOffset > \strlen ($ content )) {
190+ throw new \UnexpectedValueException ('Object stream First offset exceeds its content length. ' );
191+ }
182192
183- if ($ firstObjectOffset > \strlen ( $ content ) ) {
184- throw new \UnexpectedValueException ('Object stream First offset exceeds its content length. ' );
185- }
193+ if ($ numberOfObjects > $ firstObjectOffset ) {
194+ throw new \UnexpectedValueException ('Object stream N exceeds its index length. ' );
195+ }
186196
187- if ($ numberOfObjects > $ firstObjectOffset ) {
188- throw new \UnexpectedValueException ('Object stream N exceeds its index length. ' );
197+ $ xrefs = $ this ->parseObjectStreamIndex (
198+ substr ($ content , 0 , $ firstObjectOffset ),
199+ $ numberOfObjects
200+ );
189201 }
190202
191- $ xrefs = $ this ->parseObjectStreamIndex (
192- substr ($ content , 0 , $ firstObjectOffset ),
193- $ numberOfObjects
194- );
195203 $ content = substr ($ content , $ firstObjectOffset );
196204 $ table = [];
197205
@@ -251,46 +259,122 @@ protected function parseObject(string $id, array $structure, ?Document $document
251259 }
252260 }
253261
254- private function objectStreamInteger (Header $ header , string $ name ): int
262+ private function objectStreamInteger (array $ headerStructure , string $ name ): ? int
255263 {
256- $ value = $ header -> get ( $ name )-> getContent ( );
264+ $ count = \count ( $ headerStructure );
257265
258- if ((!\is_int ($ value ) && !\is_float ($ value ))
259- || !\is_finite ((float ) $ value )
260- || $ value < 0
261- || \floor ((float ) $ value ) !== (float ) $ value
262- || $ value > \PHP_INT_MAX
263- ) {
264- throw new \UnexpectedValueException ('Object stream ' .$ name .' must be a non-negative integer. ' );
266+ for ($ position = 0 ; $ position + 1 < $ count ; $ position += 2 ) {
267+ if ('/ ' !== $ headerStructure [$ position ][0 ] || $ name !== $ headerStructure [$ position ][1 ]) {
268+ continue ;
269+ }
270+
271+ if ('numeric ' !== $ headerStructure [$ position + 1 ][0 ]) {
272+ return null ;
273+ }
274+
275+ return $ this ->objectStreamToken ($ headerStructure [$ position + 1 ][1 ]);
265276 }
266277
267- return ( int ) $ value ;
278+ return null ;
268279 }
269280
270281 /**
271282 * @return array<int, array{0: int, 1: int}>
272283 */
273284 private function parseObjectStreamIndex (string $ index , int $ numberOfObjects ): array
274285 {
275- $ trimmedIndex = trim ($ index );
276- $ tokens = '' === $ trimmedIndex ? [] : preg_split ('/\s+/ ' , $ trimmedIndex );
277-
278- if (false === $ tokens || \count ($ tokens ) !== $ numberOfObjects * 2 ) {
279- throw new \UnexpectedValueException ('Object stream index does not match its N value. ' );
280- }
281-
282286 $ xrefs = [];
287+ $ cursor = 0 ;
288+ $ length = \strlen ($ index );
289+ $ this ->skipObjectStreamWhitespace ($ index , $ length , $ cursor );
283290
284291 for ($ position = 0 ; $ position < $ numberOfObjects ; ++$ position ) {
285292 $ xrefs [] = [
286- $ this ->objectStreamToken ( $ tokens [ $ position * 2 ] ),
287- $ this ->objectStreamToken ( $ tokens [ $ position * 2 + 1 ] ),
293+ $ this ->readObjectStreamInteger ( $ index , $ length , $ cursor ),
294+ $ this ->readObjectStreamInteger ( $ index , $ length , $ cursor ),
288295 ];
289296 }
290297
298+ if ($ cursor !== $ length ) {
299+ throw new \UnexpectedValueException ('Object stream index does not match its N value. ' );
300+ }
301+
291302 return $ xrefs ;
292303 }
293304
305+ /**
306+ * @return array{0: array<int, array{0: int, 1: int}>, 1: int}
307+ */
308+ private function parseObjectStreamIndexWithoutMetadata (string $ content ): array
309+ {
310+ $ xrefs = [];
311+ $ cursor = 0 ;
312+ $ length = \strlen ($ content );
313+ $ this ->skipObjectStreamWhitespace ($ content , $ length , $ cursor );
314+
315+ while (null !== ($ objectId = $ this ->tryReadObjectStreamInteger ($ content , $ length , $ cursor ))) {
316+ $ offset = $ this ->tryReadObjectStreamInteger ($ content , $ length , $ cursor );
317+
318+ if (null === $ offset ) {
319+ throw new \UnexpectedValueException ('Object stream index does not contain complete object references. ' );
320+ }
321+
322+ $ xrefs [] = [$ objectId , $ offset ];
323+ }
324+
325+ return [$ xrefs , $ cursor ];
326+ }
327+
328+ private function readObjectStreamInteger (string $ content , int $ length , int &$ cursor ): int
329+ {
330+ $ value = $ this ->tryReadObjectStreamInteger ($ content , $ length , $ cursor );
331+
332+ if (null === $ value ) {
333+ throw new \UnexpectedValueException ('Object stream index does not match its N value. ' );
334+ }
335+
336+ return $ value ;
337+ }
338+
339+ private function tryReadObjectStreamInteger (string $ content , int $ length , int &$ cursor ): ?int
340+ {
341+ if ($ cursor >= $ length || $ content [$ cursor ] < '0 ' || $ content [$ cursor ] > '9 ' ) {
342+ return null ;
343+ }
344+
345+ $ start = $ cursor ;
346+
347+ while ($ cursor < $ length && $ content [$ cursor ] >= '0 ' && $ content [$ cursor ] <= '9 ' ) {
348+ ++$ cursor ;
349+ }
350+
351+ if ($ cursor < $ length && !$ this ->isObjectStreamWhitespace ($ content [$ cursor ])) {
352+ throw new \UnexpectedValueException ('Object stream index values must be non-negative integers. ' );
353+ }
354+
355+ $ value = $ this ->objectStreamToken (substr ($ content , $ start , $ cursor - $ start ));
356+ $ this ->skipObjectStreamWhitespace ($ content , $ length , $ cursor );
357+
358+ return $ value ;
359+ }
360+
361+ private function skipObjectStreamWhitespace (string $ content , int $ length , int &$ cursor ): void
362+ {
363+ while ($ cursor < $ length && $ this ->isObjectStreamWhitespace ($ content [$ cursor ])) {
364+ ++$ cursor ;
365+ }
366+ }
367+
368+ private function isObjectStreamWhitespace (string $ character ): bool
369+ {
370+ return "\0" === $ character
371+ || "\t" === $ character
372+ || "\n" === $ character
373+ || "\f" === $ character
374+ || "\r" === $ character
375+ || ' ' === $ character ;
376+ }
377+
294378 private function objectStreamToken (string $ token ): int
295379 {
296380 $ normalized = ltrim ($ token , '0 ' );
0 commit comments