xref: /php-src/ext/curl/sync-constants.php (revision ba748e7b)
1#!/usr/bin/env php
2<?php
3
4/**
5 * This script checks the constants defined in the curl PHP extension, against those documented on the cURL website:
6 * https://curl.se/libcurl/c/symbols-in-versions.html
7 *
8 * See the discussion at: https://github.com/php/php-src/pull/2961
9 */
10
11const CURL_DOC_FILE = 'https://curl.se/libcurl/c/symbols-in-versions.html';
12
13const SOURCE_FILE = __DIR__ . '/curl_arginfo.h';
14
15const MIN_SUPPORTED_CURL_VERSION = '7.61.0';
16
17const IGNORED_CURL_CONSTANTS = [
18    'CURLOPT_PROGRESSDATA',
19    'CURLOPT_XFERINFODATA',
20    'CURLOPT_PREREQDATA',
21    'CURLOPT_DEBUGDATA',
22];
23
24const IGNORED_PHP_CONSTANTS = [
25    'CURLOPT_BINARYTRANSFER',
26    'CURLOPT_RETURNTRANSFER',
27    'CURLOPT_SAFE_UPLOAD',
28];
29
30const CONSTANTS_REGEX_PATTERN = '~^CURL(?:E|INFO|OPT|_VERSION|_HTTP)_[A-Z0-9_]+$~';
31
32/**
33 * A simple helper to create ASCII tables.
34 * It assumes that the same number of columns is always given to add().
35 */
36class AsciiTable
37{
38    /**
39     * @var array
40     */
41    private $values = [];
42
43    /**
44     * @var array
45     */
46    private $length = [];
47
48    /**
49     * @var int
50     */
51    private $padding = 4;
52
53    /**
54     * @param string[] $values
55     *
56     * @return void
57     */
58    public function add(string ...$values) : void
59    {
60        $this->values[] = $values;
61
62        foreach ($values as $key => $value) {
63            $length = strlen($value);
64
65            if (isset($this->length[$key])) {
66                $this->length[$key] = max($this->length[$key], $length);
67            } else {
68                $this->length[$key] = $length;
69            }
70        }
71    }
72
73    /**
74     * @return string
75     */
76    public function __toString() : string
77    {
78        $result = '';
79
80        foreach ($this->values as $values) {
81            foreach ($values as $key => $value) {
82                if ($key !== 0) {
83                    $result .= str_repeat(' ', $this->padding);
84                }
85
86                $result .= str_pad($value, $this->length[$key]);
87            }
88
89            $result .= "\n";
90        }
91
92        return $result;
93    }
94}
95
96$curlConstants   = getCurlConstants();
97$sourceConstants = getSourceConstants();
98
99$notInPHP  = []; // In cURL doc, but missing from PHP
100$notInCurl = []; // In the PHP source, but not in the cURL doc
101$outdated  = []; // In the PHP source, but removed before the minimum supported cURL version
102
103foreach ($curlConstants as $name => [$introduced, $deprecated, $removed]) {
104    $inPHP = in_array($name, $sourceConstants);
105
106    if ($removed !== null) {
107        if (version_compare($removed, MIN_SUPPORTED_CURL_VERSION) <= 0) {
108            // constant removed before the minimum supported version
109            continue;
110        }
111    }
112
113    if (! $inPHP) {
114        $notInPHP[$name] = [$introduced, $removed];
115    }
116}
117
118foreach ($sourceConstants as $name) {
119    if (! isset($curlConstants[$name])) {
120        $notInCurl[] = $name;
121        continue;
122    }
123
124    $removed = $curlConstants[$name][2];
125
126    if ($removed === null) {
127        continue;
128    }
129
130    if (version_compare($removed, MIN_SUPPORTED_CURL_VERSION) <= 0) {
131        // constant removed before the minimum supported version
132        $outdated[$name] = $removed;
133    }
134}
135
136$allGood = true;
137
138if ($notInPHP) {
139    uasort($notInPHP, function($a, $b) {
140        return version_compare($a[0], $b[0]);
141    });
142
143    $table = new AsciiTable();
144    $table->add('Constant', 'Introduced', '', 'Removed', '');
145
146    foreach ($notInPHP as $name => [$introduced, $removed]) {
147        if ($removed === null) {
148            $removed = '';
149            $removedHex = '';
150        } else {
151            $removedHex = getHexVersion($removed);
152        }
153
154        $table->add($name, $introduced, getHexVersion($introduced), $removed, $removedHex);
155    }
156
157    echo "Constants missing from the PHP source:\n\n";
158    echo $table, "\n";
159
160    $allGood = false;
161}
162
163if ($notInCurl) {
164    $table = new AsciiTable();
165
166    foreach ($notInCurl as $name) {
167        $table->add($name);
168    }
169
170    echo "Constants defined in the PHP source, but absent from the cURL documentation:\n\n";
171    echo $table, "\n";
172
173    $allGood = false;
174}
175
176if ($outdated) {
177    uasort($outdated, function($a, $b) {
178        return version_compare($a, $b);
179    });
180
181    $table = new AsciiTable();
182    $table->add('Constant', 'Removed');
183
184    foreach ($outdated as $name => $version) {
185        $table->add($name, $version);
186    }
187
188    echo "Constants defined in the PHP source, but removed before the minimum supported cURL version:\n\n";
189    echo $table, "\n";
190
191    $allGood = false;
192}
193
194if ($allGood) {
195    echo "All good! Source code and cURL documentation are in sync.\n";
196}
197
198/**
199 * Loads and parses the cURL constants from the online documentation.
200 *
201 * The result is an associative array where the key is the constant name, and the value is a numeric array with:
202 * - the introduced version
203 * - the deprecated version (nullable)
204 * - the removed version (nullable)
205 *
206 * @return array
207 */
208function getCurlConstants() : array
209{
210    $html = file_get_contents(CURL_DOC_FILE);
211
212    // Extract the constant list from the HTML file (located in the only <pre> tag in the page)
213    preg_match('~<table>(.*?)</table>~s', $html, $matches);
214    $constantList = $matches[1];
215
216    /**
217     * Parse the cURL constant lines. Possible formats:
218     *
219     * Name                            Introduced    Deprecated  Removed
220     * CURLOPT_CRLFILE                 7.19.0
221     * CURLOPT_DNS_USE_GLOBAL_CACHE    7.9.3         7.11.1
222     * CURLOPT_FTPASCII                7.1           7.11.1      7.15.5
223     * CURLOPT_HTTPREQUEST             7.1           -           7.15.5
224     */
225    $regexp = '@<tr><td>(?:<a href=".*?">)?(?<const>[A-Za-z0-9_]+)(?:</a>)?</td><td>(?:<a href=".*?">)?(?<added>[\d\.]+)(?:</a>)?</td><td>(?:<a href=".*?">)?(?<deprecated>[\d\.]+)?(?:</a>)?</td><td>(<a href=".*?">)?(?<removed>[\d\.]+)?(</a>)?</td></tr>@m';
226    preg_match_all($regexp, $constantList, $matches, PREG_SET_ORDER);
227
228    $constants = [];
229
230    foreach ($matches as $match) {
231        $name       = $match['const'];
232        $introduced = $match['added'];
233        $deprecated = $match['deprecated'] ?? null;
234        $removed    = $match['removed'] ?? null;
235
236        if (in_array($name, IGNORED_CURL_CONSTANTS, true) || !preg_match(CONSTANTS_REGEX_PATTERN, $name)) {
237            // not a wanted constant
238            continue;
239        }
240
241        if ($deprecated === '-') { // deprecated version can be a hyphen
242            $deprecated = null;
243        }
244
245        $constants[$name] = [$introduced, $deprecated, $removed];
246    }
247
248    return $constants;
249}
250
251/**
252 * Parses the defined cURL constants from the PHP extension source code.
253 *
254 * The result is a numeric array whose values are the constant names.
255 *
256 * @return array
257 */
258function getSourceConstants() : array
259{
260    $source = file_get_contents(SOURCE_FILE);
261
262    preg_match_all('/REGISTER_LONG_CONSTANT\(\"\w+\", (\w+), .+\)/', $source, $matches);
263
264    $constants = [];
265
266    foreach ($matches[1] as $name) {
267        if ($name === '__c') { // macro
268            continue;
269        }
270
271        if (in_array($name, IGNORED_PHP_CONSTANTS, true) || !preg_match(CONSTANTS_REGEX_PATTERN, $name)) {
272            // not a wanted constant
273            continue;
274        }
275
276        $constants[] = $name;
277    }
278
279    return $constants;
280}
281
282/**
283 * Converts a version number to its hex representation as used in the extension source code.
284 *
285 * Example: 7.25.1 => 0x071901
286 *
287 * @param string $version
288 *
289 * @return string
290 *
291 * @throws \RuntimeException
292 */
293function getHexVersion(string $version) : string
294{
295    $parts = explode('.', $version);
296
297    if (count($parts) === 2) {
298        $parts[] = '0';
299    }
300
301    if (count($parts) !== 3) {
302        throw new \RuntimeException('Invalid version number: ' . $version);
303    }
304
305    $hex = '0x';
306
307    foreach ($parts as $value) {
308        if (! ctype_digit($value) || strlen($value) > 3) {
309            throw new \RuntimeException('Invalid version number: ' . $version);
310        }
311
312        $value = (int) $value;
313
314        if ($value > 255) {
315            throw new \RuntimeException('Invalid version number: ' . $version);
316        }
317
318        $value = dechex($value);
319
320        if (strlen($value) === 1) {
321            $value = '0' . $value;
322        }
323
324        $hex .= $value;
325    }
326
327    return $hex;
328}
329