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