PEMBundle::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\XMLSecurity\CryptoEncoding;
6
7
use ArrayIterator;
8
use Countable;
9
use IteratorAggregate;
10
use LogicException;
11
use SimpleSAML\XMLSecurity\Assert\Assert;
12
use SimpleSAML\XMLSecurity\Exception\IOException;
13
use UnexpectedValueException;
14
15
use function array_map;
16
use function array_merge;
17
use function base64_decode;
18
use function count;
19
use function file_get_contents;
20
use function implode;
21
use function is_readable;
22
use function preg_match_all;
23
use function preg_replace;
24
25
/**
26
 * Container for multiple PEM objects.
27
 *
28
 * The order of PEMs shall be retained, eg. when read from a file.
29
 *
30
 * @phpstan-implements IteratorAggregate<int, \SimpleSAML\XMLSecurity\CryptoEncoding\PEM>
31
 */
32
class PEMBundle implements Countable, IteratorAggregate
33
{
34
    /**
35
     * Array of PEM objects.
36
     *
37
     * @var \SimpleSAML\XMLSecurity\CryptoEncoding\PEM[]
38
     */
39
    protected array $pems;
40
41
42
    /**
43
     * Constructor.
44
     *
45
     * @param PEM ...$pems
46
     */
47
    public function __construct(PEM ...$pems)
48
    {
49
        $this->pems = $pems;
50
    }
51
52
53
    /**
54
     * @return string
55
     */
56
    public function __toString(): string
57
    {
58
        return $this->string();
59
    }
60
61
62
    /**
63
     * Initialize from a string.
64
     *
65
     * @param string $str
66
     *
67
     * @throws \UnexpectedValueException
68
     *
69
     * @return self
70
     */
71
    public static function fromString(string $str): self
72
    {
73
        if (!preg_match_all(PEM::PEM_REGEX, $str, $matches, PREG_SET_ORDER)) {
74
            throw new UnexpectedValueException('No PEM blocks.');
75
        }
76
77
        $pems = array_map(
78
            function ($match) {
79
                $payload = preg_replace('/\s+/', '', $match[2]);
80
                Assert::validBase64($payload);
81
82
                $data = base64_decode($payload, true);
83
                if (empty($data)) {
84
                    throw new UnexpectedValueException(
85
                        'Failed to decode PEM data.',
86
                    );
87
                }
88
                return new PEM($match[1], $data);
89
            },
90
            $matches,
91
        );
92
93
        return new self(...$pems);
94
    }
95
96
97
    /**
98
     * Initialize from a file.
99
     *
100
     * @param string $filename
101
     *
102
     * @throws \RuntimeException If file reading fails
103
     *
104
     * @return self
105
     */
106
    public static function fromFile(string $filename): self
107
    {
108
        error_clear_last();
109
        $str = @file_get_contents($filename);
110
111
        if (!is_readable($filename) || ($str === false)) {
112
            $e = error_get_last();
113
            $error = $e['message'] ?? "Check that the file exists and can be read.";
114
            throw new IOException(sprintf("File '%s' was not loaded;  %s", $filename, $error));
115
        }
116
117
        return self::fromString($str);
118
    }
119
120
121
    /**
122
     * Get self with PEM objects appended.
123
     *
124
     * @param \SimpleSAML\XMLSecurity\CryptoEncoding\PEM ...$pems
125
     *
126
     * @return self
127
     */
128
    public function withPEMs(PEM ...$pems): self
129
    {
130
        $obj = clone $this;
131
        $obj->pems = array_merge($obj->pems, $pems);
132
133
        return $obj;
134
    }
135
136
137
    /**
138
     * Get all PEMs in a bundle.
139
     *
140
     * @return \SimpleSAML\XMLSecurity\CryptoEncoding\PEM[]
141
     */
142
    public function all(): array
143
    {
144
        return $this->pems;
145
    }
146
147
148
    /**
149
     * Get the first PEM in a bundle.
150
     *
151
     * @throws \LogicException If bundle contains no PEM objects
152
     *
153
     * @return \SimpleSAML\XMLSecurity\CryptoEncoding\PEM
154
     */
155
    public function first(): PEM
156
    {
157
        if (!count($this->pems)) {
158
            throw new LogicException('No PEMs.');
159
        }
160
161
        return $this->pems[0];
162
    }
163
164
165
    /**
166
     * Get the last PEM in a bundle.
167
     *
168
     * @throws \LogicException If bundle contains no PEM objects
169
     *
170
     * @return \SimpleSAML\XMLSecurity\CryptoEncoding\PEM
171
     */
172
    public function last(): PEM
173
    {
174
        if (!count($this->pems)) {
175
            throw new LogicException('No PEMs.');
176
        }
177
178
        return $this->pems[count($this->pems) - 1];
179
    }
180
181
182
    /**
183
     * @see \Countable::count()
184
     *
185
     * @return int
186
     */
187
    public function count(): int
188
    {
189
        return count($this->pems);
190
    }
191
192
193
    /**
194
     * Get iterator for PEMs.
195
     *
196
     * @see \IteratorAggregate::getIterator()
197
     *
198
     * @return ArrayIterator<int, \SimpleSAML\XMLSecurity\CryptoEncoding\PEM>
199
     */
200
    public function getIterator(): ArrayIterator
201
    {
202
        return new ArrayIterator($this->pems);
203
    }
204
205
206
    /**
207
     * Encode bundle to a string of contiguous PEM blocks.
208
     *
209
     * @return string
210
     */
211
    public function string(): string
212
    {
213
        return implode(
214
            "\n",
215
            array_map(
216
                function (PEM $pem) {
217
                    return $pem->string();
218
                },
219
                $this->pems,
220
            ),
221
        );
222
    }
223
}
224