PEMBundle::last()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 7
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 \SimpleSAML\XMLSecurity\CryptoEncoding\PEM ...$pems
46
     */
47
    public function __construct(PEM ...$pems)
48
    {
49
        $this->pems = $pems;
50
    }
51
52
53
    /**
54
     */
55
    public function __toString(): string
56
    {
57
        return $this->string();
58
    }
59
60
61
    /**
62
     * Initialize from a string.
63
     *
64
     * @throws \UnexpectedValueException
65
     */
66
    public static function fromString(string $str): self
67
    {
68
        if (!preg_match_all(PEM::PEM_REGEX, $str, $matches, PREG_SET_ORDER)) {
69
            throw new UnexpectedValueException('No PEM blocks.');
70
        }
71
72
        $pems = array_map(
73
            function ($match) {
74
                $payload = preg_replace('/\s+/', '', $match[2]);
75
                Assert::validBase64($payload);
76
77
                $data = base64_decode($payload, true);
78
                if (empty($data)) {
79
                    throw new UnexpectedValueException(
80
                        'Failed to decode PEM data.',
81
                    );
82
                }
83
                return new PEM($match[1], $data);
84
            },
85
            $matches,
86
        );
87
88
        return new self(...$pems);
89
    }
90
91
92
    /**
93
     * Initialize from a file.
94
     *
95
     * @throws \RuntimeException If file reading fails
96
     */
97
    public static function fromFile(string $filename): self
98
    {
99
        error_clear_last();
100
        $str = @file_get_contents($filename);
101
102
        if (!is_readable($filename) || ($str === false)) {
103
            $e = error_get_last();
104
            $error = $e['message'] ?? "Check that the file exists and can be read.";
105
            throw new IOException(sprintf("File '%s' was not loaded;  %s", $filename, $error));
106
        }
107
108
        return self::fromString($str);
109
    }
110
111
112
    /**
113
     * Get self with PEM objects appended.
114
     *
115
     * @param \SimpleSAML\XMLSecurity\CryptoEncoding\PEM ...$pems
116
     *
117
     * @return self
118
     */
119
    public function withPEMs(PEM ...$pems): self
120
    {
121
        $obj = clone $this;
122
        $obj->pems = array_merge($obj->pems, $pems);
123
124
        return $obj;
125
    }
126
127
128
    /**
129
     * Get all PEMs in a bundle.
130
     *
131
     * @return \SimpleSAML\XMLSecurity\CryptoEncoding\PEM[]
132
     */
133
    public function all(): array
134
    {
135
        return $this->pems;
136
    }
137
138
139
    /**
140
     * Get the first PEM in a bundle.
141
     *
142
     * @throws \LogicException If bundle contains no PEM objects
143
     *
144
     * @return \SimpleSAML\XMLSecurity\CryptoEncoding\PEM
145
     */
146
    public function first(): PEM
147
    {
148
        if (!count($this->pems)) {
149
            throw new LogicException('No PEMs.');
150
        }
151
152
        return $this->pems[0];
153
    }
154
155
156
    /**
157
     * Get the last PEM in a bundle.
158
     *
159
     * @throws \LogicException If bundle contains no PEM objects
160
     *
161
     * @return \SimpleSAML\XMLSecurity\CryptoEncoding\PEM
162
     */
163
    public function last(): PEM
164
    {
165
        if (!count($this->pems)) {
166
            throw new LogicException('No PEMs.');
167
        }
168
169
        return $this->pems[count($this->pems) - 1];
170
    }
171
172
173
    /**
174
     * @see \Countable::count()
175
     */
176
    public function count(): int
177
    {
178
        return count($this->pems);
179
    }
180
181
182
    /**
183
     * Get iterator for PEMs.
184
     *
185
     * @see \IteratorAggregate::getIterator()
186
     *
187
     * @return \ArrayIterator<int, \SimpleSAML\XMLSecurity\CryptoEncoding\PEM>
188
     */
189
    public function getIterator(): ArrayIterator
190
    {
191
        return new ArrayIterator($this->pems);
192
    }
193
194
195
    /**
196
     * Encode bundle to a string of contiguous PEM blocks.
197
     */
198
    public function string(): string
199
    {
200
        return implode(
201
            "\n",
202
            array_map(
203
                function (PEM $pem) {
204
                    return $pem->string();
205
                },
206
                $this->pems,
207
            ),
208
        );
209
    }
210
}
211