xref: /PHP-8.3/ext/standard/versioning.c (revision aff36587)
1 /*
2    +----------------------------------------------------------------------+
3    | Copyright (c) The PHP Group                                          |
4    +----------------------------------------------------------------------+
5    | This source file is subject to version 3.01 of the PHP license,      |
6    | that is bundled with this package in the file LICENSE, and is        |
7    | available through the world-wide-web at the following url:           |
8    | https://www.php.net/license/3_01.txt                                 |
9    | If you did not receive a copy of the PHP license and are unable to   |
10    | obtain it through the world-wide-web, please send a note to          |
11    | license@php.net so we can mail you a copy immediately.               |
12    +----------------------------------------------------------------------+
13    | Author: Stig Sæther Bakken <ssb@php.net>                             |
14    +----------------------------------------------------------------------+
15  */
16 
17 #include <stdio.h>
18 #include <sys/types.h>
19 #include <ctype.h>
20 #include <stdlib.h>
21 #include <string.h>
22 #include "php.h"
23 #include "php_versioning.h"
24 
25 /* {{{ php_canonicalize_version() */
26 
27 PHPAPI char *
php_canonicalize_version(const char * version)28 php_canonicalize_version(const char *version)
29 {
30 	size_t len = strlen(version);
31 	char *buf = safe_emalloc(len, 2, 1), *q, lp, lq;
32 	const char *p;
33 
34 	if (len == 0) {
35 		*buf = '\0';
36 		return buf;
37 	}
38 
39 	p = version;
40 	q = buf;
41 	*q++ = lp = *p++;
42 
43 	while (*p) {
44 /*  s/[-_+]/./g;
45  *  s/([^\d\.])([^\D\.])/$1.$2/g;
46  *  s/([^\D\.])([^\d\.])/$1.$2/g;
47  */
48 #define isdig(x) (isdigit(x)&&(x)!='.')
49 #define isndig(x) (!isdigit(x)&&(x)!='.')
50 #define isspecialver(x) ((x)=='-'||(x)=='_'||(x)=='+')
51 
52 		lq = *(q - 1);
53 		if (isspecialver(*p)) {
54 			if (lq != '.') {
55 				*q++ = '.';
56 			}
57 		} else if ((isndig(lp) && isdig(*p)) || (isdig(lp) && isndig(*p))) {
58 			if (lq != '.') {
59 				*q++ = '.';
60 			}
61 			*q++ = *p;
62 		} else if (!isalnum(*p)) {
63 			if (lq != '.') {
64 				*q++ = '.';
65 			}
66 		} else {
67 			*q++ = *p;
68 		}
69 		lp = *p++;
70 	}
71 	*q++ = '\0';
72 	return buf;
73 }
74 
75 /* }}} */
76 /* {{{ compare_special_version_forms() */
77 
78 typedef struct {
79 	const char *name;
80 	int order;
81 } special_forms_t;
82 
83 static int
compare_special_version_forms(char * form1,char * form2)84 compare_special_version_forms(char *form1, char *form2)
85 {
86 	int found1 = -1, found2 = -1;
87 	special_forms_t special_forms[11] = {
88 		{"dev", 0},
89 		{"alpha", 1},
90 		{"a", 1},
91 		{"beta", 2},
92 		{"b", 2},
93 		{"RC", 3},
94 		{"rc", 3},
95 		{"#", 4},
96 		{"pl", 5},
97 		{"p", 5},
98 		{NULL, 0},
99 	};
100 	special_forms_t *pp;
101 
102 	for (pp = special_forms; pp && pp->name; pp++) {
103 		if (strncmp(form1, pp->name, strlen(pp->name)) == 0) {
104 			found1 = pp->order;
105 			break;
106 		}
107 	}
108 	for (pp = special_forms; pp && pp->name; pp++) {
109 		if (strncmp(form2, pp->name, strlen(pp->name)) == 0) {
110 			found2 = pp->order;
111 			break;
112 		}
113 	}
114 	return ZEND_NORMALIZE_BOOL(found1 - found2);
115 }
116 
117 /* }}} */
118 /* {{{ php_version_compare() */
119 
120 PHPAPI int
php_version_compare(const char * orig_ver1,const char * orig_ver2)121 php_version_compare(const char *orig_ver1, const char *orig_ver2)
122 {
123 	char *ver1;
124 	char *ver2;
125 	char *p1, *p2, *n1, *n2;
126 	long l1, l2;
127 	int compare = 0;
128 
129 	if (!*orig_ver1 || !*orig_ver2) {
130 		if (!*orig_ver1 && !*orig_ver2) {
131 			return 0;
132 		} else {
133 			return *orig_ver1 ? 1 : -1;
134 		}
135 	}
136 	if (orig_ver1[0] == '#') {
137 		ver1 = estrdup(orig_ver1);
138 	} else {
139 		ver1 = php_canonicalize_version(orig_ver1);
140 	}
141 	if (orig_ver2[0] == '#') {
142 		ver2 = estrdup(orig_ver2);
143 	} else {
144 		ver2 = php_canonicalize_version(orig_ver2);
145 	}
146 	p1 = n1 = ver1;
147 	p2 = n2 = ver2;
148 	while (*p1 && *p2 && n1 && n2) {
149 		if ((n1 = strchr(p1, '.')) != NULL) {
150 			*n1 = '\0';
151 		}
152 		if ((n2 = strchr(p2, '.')) != NULL) {
153 			*n2 = '\0';
154 		}
155 		if (isdigit(*p1) && isdigit(*p2)) {
156 			/* compare element numerically */
157 			l1 = strtol(p1, NULL, 10);
158 			l2 = strtol(p2, NULL, 10);
159 			compare = ZEND_NORMALIZE_BOOL(l1 - l2);
160 		} else if (!isdigit(*p1) && !isdigit(*p2)) {
161 			/* compare element names */
162 			compare = compare_special_version_forms(p1, p2);
163 		} else {
164 			/* mix of names and numbers */
165 			if (isdigit(*p1)) {
166 				compare = compare_special_version_forms("#N#", p2);
167 			} else {
168 				compare = compare_special_version_forms(p1, "#N#");
169 			}
170 		}
171 		if (compare != 0) {
172 			break;
173 		}
174 		if (n1 != NULL) {
175 			p1 = n1 + 1;
176 		}
177 		if (n2 != NULL) {
178 			p2 = n2 + 1;
179 		}
180 	}
181 	if (compare == 0) {
182 		if (n1 != NULL) {
183 			if (isdigit(*p1)) {
184 				compare = 1;
185 			} else {
186 				compare = php_version_compare(p1, "#N#");
187 			}
188 		} else if (n2 != NULL) {
189 			if (isdigit(*p2)) {
190 				compare = -1;
191 			} else {
192 				compare = php_version_compare("#N#", p2);
193 			}
194 		}
195 	}
196 	efree(ver1);
197 	efree(ver2);
198 	return compare;
199 }
200 
201 /* }}} */
202 /* {{{ Compares two "PHP-standardized" version number strings */
203 
PHP_FUNCTION(version_compare)204 PHP_FUNCTION(version_compare)
205 {
206 	char *v1, *v2;
207 	zend_string *op = NULL;
208 	size_t v1_len, v2_len;
209 	int compare;
210 
211 	ZEND_PARSE_PARAMETERS_START(2, 3)
212 		Z_PARAM_STRING(v1, v1_len)
213 		Z_PARAM_STRING(v2, v2_len)
214 		Z_PARAM_OPTIONAL
215 		Z_PARAM_STR_OR_NULL(op)
216 	ZEND_PARSE_PARAMETERS_END();
217 
218 	compare = php_version_compare(v1, v2);
219 	if (!op) {
220 		RETURN_LONG(compare);
221 	}
222 	if (zend_string_equals_literal(op, "<") || zend_string_equals_literal(op, "lt")) {
223 		RETURN_BOOL(compare == -1);
224 	}
225 	if (zend_string_equals_literal(op, "<=") || zend_string_equals_literal(op, "le")) {
226 		RETURN_BOOL(compare != 1);
227 	}
228 	if (zend_string_equals_literal(op, ">") || zend_string_equals_literal(op, "gt")) {
229 		RETURN_BOOL(compare == 1);
230 	}
231 	if (zend_string_equals_literal(op, ">=") || zend_string_equals_literal(op, "ge")) {
232 		RETURN_BOOL(compare != -1);
233 	}
234 	if (zend_string_equals_literal(op, "==") || zend_string_equals_literal(op, "=") || zend_string_equals_literal(op, "eq")) {
235 		RETURN_BOOL(compare == 0);
236 	}
237 	if (zend_string_equals_literal(op, "!=") || zend_string_equals_literal(op, "<>") || zend_string_equals_literal(op, "ne")) {
238 		RETURN_BOOL(compare != 0);
239 	}
240 
241 	zend_argument_value_error(3, "must be a valid comparison operator");
242 }
243 
244 /* }}} */
245