Repository navigation
Expand file tree
/
Copy pathPcreTarget.php
More file actions
293 lines (262 loc) · 11.4 KB
/
Copy pathPcreTarget.php
File metadata and controls
293 lines (262 loc) · 11.4 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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
<?php
declare(strict_types=1);
/*
* This file is part of the PHPRegex package.
*
* (c) Younes ENNAJI <younes.ennaji.pro@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPRegex\Parser;
use PHPRegex\Parser\Cache\StaticCaches;
use PHPRegex\Parser\Engine\CharacterTables;
use PHPRegex\Parser\Exception\InvalidRegexOptionException;
use PHPRegex\Parser\Text\Ascii;
/**
* The PHP version, the PCRE2 release and the LC_CTYPE locale a pattern is
* judged for.
*
* What PCRE2 accepts, and where it reports an error, moved across its
* releases; a few rules belong to PHP itself. The locale names the tables
* PHP hands PCRE2, which decide the other case of each byte and the bytes
* of each type without UCP: PCRE2's own under "C", PHP's default, those the
* C library gives under any other locale setlocale() names. The engine that runs the
* analysis need not be the one that will run the pattern, so the target
* is named once and read everywhere a rule depends on it. Releases older
* than 10.39 are judged with the 10.39 rules, but for the few older changes
* the library names (judgedWith()); newer ones than the library knows with
* the newest rules it has.
*/
final class PcreTarget
{
/**
* The oldest release whose rules the library knows in full, 10.39.
*/
private const KNOWN_IN_FULL = 10_039;
public readonly string $pcreVersion;
/**
* The LC_CTYPE locale the patterns run under, as the C library names
* it: "C" for PCRE2's own tables.
*/
public readonly string $locale;
/**
* What supports() answered, by release number and feature name: the
* lexer and the parser ask it many times per pattern, and the answer
* depends on nothing else. As many entries as releases asked about times
* features.
*
* @var array<int, array<string, bool>>
*/
private static array $supported = [];
private readonly int $release;
/**
* @param int $phpVersionId a PHP_VERSION_ID, 80400 for PHP 8.4
* @param string $pcreVersion a PCRE2 release, "10.44", as PCRE_VERSION
* spells it or without its date
* @param string|null $locale an LC_CTYPE locale, "tr_TR.UTF-8"; the
* runtime's when null
*
* @throws InvalidRegexOptionException when $pcreVersion names no release, or
* $locale one this machine cannot set
*/
public function __construct(
public readonly int $phpVersionId,
string $pcreVersion,
?string $locale = null
) {
[$major, $minor] = self::releaseParts($pcreVersion)
?? throw new InvalidRegexOptionException(\sprintf('"pcre_version" must be a PCRE2 release like "10.44", not "%s".', $pcreVersion));
if ((int) $major < 10) {
throw new InvalidRegexOptionException(\sprintf('"pcre_version" %s names no PCRE2 release: the first is 10.00.', trim($pcreVersion)));
}
$this->release = (int) $major * 1000 + (int) $minor;
$this->pcreVersion = (int) $major.'.'.$minor;
$this->locale = null === $locale ? CharacterTables::current() : CharacterTables::forLocale($locale)->locale;
}
/**
* The running PHP and the PCRE2 it links, which may not be the one its
* version bundles: the PHP 8.4 packages of a distribution may link an
* older one.
*/
public static function runtime(): self
{
return new self(\PHP_VERSION_ID, \PCRE_VERSION);
}
/**
* The PHP versions, as PHP_VERSION_IDs in ascending order, at which a
* verdict of the library may change: the lowest PHP it supports, then
* each version that bundles a newer PCRE2 or where a rule changes (8.5
* refuses "\K" in a lookaround, and stops refusing "\C" under "u" until
* 8.5.10). Judging a range of PHP versions at these points judges all of
* it. The list may grow in a minor release, when the library learns a
* PHP release or a rule.
*
* @return list<int>
*/
public static function phpVersionBoundaries(): array
{
return PhpVersionGates::POINTS;
}
/**
* A PHP version with the PCRE2 its sources bundle: 10.32 for 7.3, 10.33
* for 7.4, 10.35 for 8.0, 10.39 for 8.1, 10.40 for 8.2, 10.42 for 8.3,
* 10.44 for 8.4 through 8.6. A series is given the release of its x.y.0,
* but for 8.1: 8.1.0 bundled 10.37, and 8.1.1 onwards 10.39. PHP 7.4.6
* moved to 10.34 and 7.4.12 to 10.35, judged as 10.33. PHP 7.2 and
* earlier link PCRE1, which no PCRE2 release stands for: they get 10.32.
*
* An approximation, not a promise. A PHP version never determines the
* PCRE2 a build links: distributions compile against their own
* libpcre2, and patch releases carry whatever was current when they
* were cut - 8.4.0 bundles 10.44 while 8.4.27 links 10.49. Three
* answers exist and none collapses into another: the engine the
* running PHP links (PCRE_VERSION), the one bundledWith() estimates
* for an analysed PHP, and an explicit pin the consumer holds.
* ParserOptions::resolveTarget() decides between them, for every entry
* point: the pin wins; the running engine speaks for the running PHP,
* and, in the lint, the language server and the bridges, for every PHP
* of its minor version; the estimate answers for the rest.
*/
public static function bundledWith(int $phpVersionId): self
{
$release = match (true) {
$phpVersionId >= PhpVersionGates::BUNDLES_PCRE2_10_44 => '10.44',
$phpVersionId >= PhpVersionGates::BUNDLES_PCRE2_10_42 => '10.42',
$phpVersionId >= PhpVersionGates::BUNDLES_PCRE2_10_40 => '10.40',
$phpVersionId >= PhpVersionGates::BUNDLES_PCRE2_10_39 => '10.39',
$phpVersionId >= PhpVersionGates::BUNDLES_PCRE2_10_35 => '10.35',
$phpVersionId >= PhpVersionGates::BUNDLES_PCRE2_10_33 => '10.33',
default => '10.32',
};
return new self($phpVersionId, $release);
}
/**
* The same PHP and PCRE2 under another locale, the runtime's for null.
*
* @throws InvalidRegexOptionException when $locale names one this machine
* cannot set
*/
public function withLocale(?string $locale): self
{
return new self($this->phpVersionId, $this->pcreVersion, $locale);
}
/**
* The PCRE2 release whose rules judge this one: its own, or 10.39 for
* an older release. The library knows the rules of 10.39 and later in
* full; of an older release, only the few changes it names.
*/
public function judgedWith(): string
{
return $this->release < self::KNOWN_IN_FULL ? '10.39' : $this->pcreVersion;
}
/**
* Whether the PCRE2 judged is at least $release, as "10.47".
*/
public function pcreAtLeast(string $release): bool
{
// "10.4" would read as 10.04 and hold for every release: a minor is
// written with two digits at least, as PCRE2 writes it.
[, $minor] = explode('.', $release, 2) + [1 => ''];
$number = \strlen($minor) >= 2 ? self::releaseNumber($release) : null;
if (null === $number) {
throw new InvalidRegexOptionException(\sprintf('"%s" is not a PCRE2 release like "10.45".', $release));
}
return $this->release >= $number;
}
/**
* Whether the PCRE2 judged has a behaviour: it arrived in that release
* or an earlier one.
*/
public function supports(PcreFeature $feature): bool
{
if (isset(self::$supported[$this->release][$feature->name])) {
return self::$supported[$this->release][$feature->name];
}
StaticCaches::register(self::class, static function (): void {
self::$supported = [];
});
return self::$supported[$this->release][$feature->name] = $this->pcreAtLeast($feature->release());
}
/**
* Where PCRE2 reports an error it reports past the character at fault
* from 10.47, given that later offset: $shift bytes earlier, on the
* character itself, before 10.47, which every PHP release bundles. A
* character of several bytes in UTF mode moves back whole.
*
* @internal
*/
public function faultOffset(int $offset, int $shift = 1): int
{
return $this->supports(PcreFeature::ErrorOffsetPastTheFault) ? $offset : $offset - $shift;
}
/**
* Whether the running engine is the one judged: the same PHP minor
* version, linking the same PCRE2 release, under the same locale. Only
* then can PHP itself be asked about a pattern.
*/
public function isRunningEngine(): bool
{
$running = self::runtime();
return intdiv($this->phpVersionId, 100) === intdiv($running->phpVersionId, 100)
&& $this->release === $running->release
&& $this->locale === $running->locale;
}
/**
* What a cached tree was read for: the PHP major and minor version, as
* no parse rule depends on a patch release, the PCRE2 release, and the
* locale but "C", whose tables may judge a pattern otherwise. The rules
* that depend on a patch release, such as "\C" under "u" from PHP
* 8.4.25, belong to the validator, which runs on every call, cached
* tree or not.
*/
public function cacheKey(): string
{
return \sprintf(
'php%d.%d/pcre%d.%d%s',
intdiv($this->phpVersionId, 10000),
intdiv($this->phpVersionId, 100) % 100,
intdiv($this->release, 1000),
$this->release % 1000,
CharacterTables::BUILT_IN === $this->locale ? '' : '/'.$this->locale,
);
}
/**
* What tells this target from another: the PHP_VERSION_ID, the PCRE2
* release and the locale, as "80426/10.44/C".
*/
public function key(): string
{
return $this->phpVersionId.'/'.$this->pcreVersion.'/'.$this->locale;
}
private static function releaseNumber(string $version): ?int
{
$parts = self::releaseParts($version);
return null === $parts ? null : (int) $parts[0] * 1000 + (int) $parts[1];
}
/**
* The major and minor digits of a release, read without the engine: a
* backtrack limit set low on the host must not make it unreadable.
*
* @return array{string, string}|null
*/
private static function releaseParts(string $version): ?array
{
$whitespace = " \t\n\v\f\r";
$release = ltrim($version, $whitespace);
$release = substr($release, 0, strcspn($release, $whitespace));
// A pre-release build is spelled "10.45-RC1 2024-12-01".
$hyphen = strpos($release, '-');
$build = false === $hyphen ? null : substr($release, $hyphen + 1);
$parts = explode('.', false === $hyphen ? $release : substr($release, 0, $hyphen));
// PCRE2 10 writes its minor with two digits: "10.4" names no
// release, and would read as 10.04.
if (2 !== \count($parts) || !Ascii::isDigit($parts[0]) || !Ascii::isDigit($parts[1]) || ('10' === ltrim($parts[0], '0') && \strlen($parts[1]) < 2)) {
return null;
}
if (null !== $build && !Ascii::isAlnum($build)) {
return null;
}
return [$parts[0], $parts[1]];
}
}