Passed
Pull Request — master (#43)
by Tim
02:10
created

PEMBundle::fromFile()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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