Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.10% covered (success)
87.10%
27 / 31
75.00% covered (success)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Registry
87.10% covered (success)
87.10%
27 / 31
75.00% covered (success)
75.00%
3 / 4
26.34
0.00% covered (danger)
0.00%
0 / 1
 decode
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
8
 filterName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 normalizeParams
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
8.74
 resolve
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
11
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Pdf\Extract\Filter;
16
17use Pop\Pdf\Extract\Exception;
18use Pop\Pdf\Extract\Value;
19
20/**
21 * Pdf extract filter registry class
22 *
23 * @category   Pop
24 * @package    Pop\Pdf
25 * @author     Nick Sagona, III <nick@popphp.org>
26 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
27 * @license    https://www.popphp.org/license     New BSD License
28 * @version    6.0.0
29 */
30class Registry
31{
32
33    /**
34     * Decode stream data through one or more filters
35     *
36     * @param  string  $data
37     * @param  mixed   $filter
38     * @param  mixed   $decodeParms
39     * @param  ?Budget $budget
40     * @throws Exception
41     * @return string
42     */
43    public static function decode(string $data, mixed $filter, mixed $decodeParms = null, ?Budget $budget = null): string
44    {
45        // A zero-cost charge fails fast against an already-exhausted budget
46        // before any filter work runs - without this, every call after
47        // exhaustion still pays the full decode cost (CPU + a transient
48        // buffer) only to throw afterward, which a caller that loops (many
49        // /Contents refs, many Do operators) turns back into unbounded work.
50        $budget?->charge(0);
51
52        if ($filter === null) {
53            // An unfiltered stream is still attacker-controlled size that
54            // must count toward the aggregate budget - omitting /Filter
55            // entirely (a valid, common case for genuinely uncompressed
56            // streams) must not be a free way to bypass it, since the real
57            // risk here is many streams accumulating, not per-call
58            // compression amplification.
59            $budget?->charge(strlen($data));
60            return $data;
61        }
62
63        $filters   = ($filter instanceof Value\Name) ? [$filter] : $filter;
64        $parmsList = (($decodeParms === null) || ($decodeParms instanceof Value\Name) ||
65            (is_array($decodeParms) && !array_is_list($decodeParms))) ? [$decodeParms] : $decodeParms;
66
67        foreach ($filters as $i => $filterName) {
68            $name  = self::filterName($filterName);
69            $parms = self::normalizeParams($parmsList[$i] ?? null);
70            $data  = self::resolve($name)->decode($data, $parms);
71            $budget?->charge(strlen($data));
72        }
73
74        return $data;
75    }
76
77    /**
78     * Resolve a filter value (Name or raw string) to its filter name
79     *
80     * @param  mixed $filter
81     * @return string
82     */
83    protected static function filterName(mixed $filter): string
84    {
85        return ($filter instanceof Value\Name) ? $filter->name : (string) $filter;
86    }
87
88    /**
89     * Normalize a /DecodeParms value into a plain array of scalar params
90     *
91     * @param  mixed $parms
92     * @return array
93     */
94    protected static function normalizeParams(mixed $parms): array
95    {
96        if (!is_array($parms)) {
97            return [];
98        }
99
100        $out = [];
101        foreach ($parms as $key => $value) {
102            $out[$key] = ($value instanceof Value\Name) ? $value->name : $value;
103        }
104
105        return $out;
106    }
107
108    /**
109     * Resolve a filter name to a filter instance
110     *
111     * @param  string $name
112     * @throws Exception
113     * @return FilterInterface
114     */
115    protected static function resolve(string $name): FilterInterface
116    {
117        if (($name === 'FlateDecode') || ($name === 'Fl')) {
118            return new Flate();
119        } elseif (($name === 'ASCIIHexDecode') || ($name === 'AHx')) {
120            return new AsciiHex();
121        } elseif (($name === 'ASCII85Decode') || ($name === 'A85')) {
122            return new Ascii85();
123        } elseif (($name === 'RunLengthDecode') || ($name === 'RL')) {
124            return new RunLength();
125        } elseif (($name === 'LZWDecode') || ($name === 'LZW')) {
126            return new Lzw();
127        }
128
129        throw new Exception("Error: Unsupported stream filter '{$name}'.");
130    }
131
132}