forked from Skillshare/formatphp
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathIdInterpolator.php
More file actions
173 lines (152 loc) · 5.57 KB
/
IdInterpolator.php
File metadata and controls
173 lines (152 loc) · 5.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
<?php
/**
* This file is part of formatphp/formatphp
*
* formatphp/formatphp is open source software: you can distribute
* it and/or modify it under the terms of the MIT License
* (the "License"). You may not use this file except in
* compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*
* @copyright Copyright (c) Skillshare, Inc. <https://www.skillshare.com>
* @copyright Copyright (c) FormatPHP Contributors <https://formatphp.dev>
* @license https://opensource.org/licenses/MIT MIT License
*/
declare(strict_types=1);
namespace FormatPHP\Extractor;
use Closure;
use FormatPHP\DescriptorInterface;
use FormatPHP\Exception\InvalidArgumentException;
use FormatPHP\Exception\UnableToGenerateMessageIdException;
use function base64_encode;
use function bin2hex;
use function count;
use function hash;
use function hash_algos;
use function in_array;
use function preg_match;
use function preg_replace;
use function rtrim;
use function sprintf;
use function strlen;
use function strtr;
use function substr;
use function trim;
/**
* IdInterpolator supports generation of message descriptor IDs
*
* If message descriptors are missing the id property, we will use this
* pattern to automatically generate IDs for them.
*
* The pattern follows this format:
*
* [hashAlgorithm:contenthash:encodingAlgorithm:length]
*
* When passing this value, provide the hashAlgorithm, encodingAlgorithm,
* and length, and formatphp will calculate the contenthash.
*
* For example, if you wish to use `haval160,4` as the hashing algorithm,
* `hex` as the encoding algorithm, with a length of 10, you would pass
* the following string:
*
* [haval160,4:contenthash:hex:10]
*
* See <https://www.php.net/hash_algos> for available hashing algorithms.
*
* The following binary-to-text encodings are supported:
*
* - `base64`
* - `base64url`
* - `hex`
*/
class IdInterpolator
{
public const DEFAULT_ID_INTERPOLATION_PATTERN = '[sha512:contenthash:base64:6]';
private const PATTERN_MATCHER = '/\[(?:([^:\]]+):)?(?:hash|contenthash)(?::([a-z0-9]+))?(?::(\d+))?\]/';
/**
* Generates and returns an ID for the given message descriptor
*
* If the message descriptor already has an ID, we do not generate a new
* one; we return the current one, instead.
*
* If the message descriptor does not have a default message, we cannot
* generate an ID, so we throw `UnableToGenerateMessageId`.
*
* @throws InvalidArgumentException
* @throws UnableToGenerateMessageIdException
*/
public function generateId(
DescriptorInterface $descriptor,
string $idInterpolationPattern = self::DEFAULT_ID_INTERPOLATION_PATTERN
): string {
if ($descriptor->getId() !== null) {
return (string) $descriptor->getId();
}
$contentHash = $this->buildContentHash($descriptor);
$options = $this->parsePattern($idInterpolationPattern);
if (!in_array($options->hashingAlgorithm, hash_algos())) {
throw new InvalidArgumentException(sprintf(
'Unknown or unsupported hashing algorithm: "%s".',
$options->hashingAlgorithm,
));
}
$encoder = $this->getEncoder($options->encodingAlgorithm);
$binaryHash = hash($options->hashingAlgorithm, $contentHash, true);
return substr($encoder($binaryHash), 0, $options->length);
}
/**
* @return Closure(string):string
*
* @throws InvalidArgumentException
*/
private function getEncoder(string $encodingType): Closure
{
switch ($encodingType) {
case 'base64':
return fn (string $data): string => base64_encode($data);
case 'base64url':
return fn (string $data): string => rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
case 'hex':
return fn (string $data): string => bin2hex($data);
}
throw new InvalidArgumentException(sprintf('Unknown or unsupported encoding algorithm: "%s".', $encodingType));
}
/**
* @throws UnableToGenerateMessageIdException
*/
private function buildContentHash(DescriptorInterface $descriptor): string
{
$data = '';
if ($descriptor->getDefaultMessage() !== null) {
$data .= trim((string) preg_replace('/\n\s*/', ' ', (string) $descriptor->getDefaultMessage()));
}
if (strlen($data) > 0 && $descriptor->getDescription() !== null) {
$data .= '#' . (string) $descriptor->getDescription();
}
if ($data === '') {
throw new UnableToGenerateMessageIdException(
'To auto-generate a message ID, the message descriptor must '
. 'have a default message and, optionally, a description.',
);
}
return $data;
}
/**
* @throws InvalidArgumentException
*/
private function parsePattern(string $pattern): IdInterpolatorOptions
{
preg_match(self::PATTERN_MATCHER, $pattern, $matches);
if (count($matches) !== 4) {
throw new InvalidArgumentException(
sprintf('Pattern is not a valid ID interpolation pattern: "%s".', $pattern),
);
}
return new IdInterpolatorOptions($matches[1], $matches[2], (int) $matches[3]);
}
}