xref: /freebsd/contrib/pkgconf/libpkgconf/version.c (revision 592efe252472a3385acf36b1f49ecf710a7f3d9c)
1 /*
2  * version.c
3  * version comparison
4  *
5  * SPDX-License-Identifier: pkgconf
6  *
7  * Copyright (c) 2011-2026 pkgconf authors (see AUTHORS).
8  *
9  * Permission to use, copy, modify, and/or distribute this software for any
10  * purpose with or without fee is hereby granted, provided that the above
11  * copyright notice and this permission notice appear in all copies.
12  *
13  * This software is provided 'as is' and without any warranty, express or
14  * implied.  In no event shall the authors be liable for any damages arising
15  * from the use of this software.
16  */
17 
18 #include <libpkgconf/config.h>
19 #include <libpkgconf/stdinc.h>
20 #include <libpkgconf/libpkgconf.h>
21 
22 typedef enum {
23 	PKGCONF_VERSION_TOKEN_END = 0,
24 	PKGCONF_VERSION_TOKEN_TILDE,
25 	PKGCONF_VERSION_TOKEN_NUMERIC,
26 	PKGCONF_VERSION_TOKEN_ALPHA
27 } pkgconf_version_token_kind_t;
28 
29 typedef struct {
30 	pkgconf_version_token_kind_t kind;
31 	const char *start;
32 	const char *end;
33 } pkgconf_version_token_t;
34 
35 typedef struct {
36 	const char *cur;
37 } pkgconf_version_iter_t;
38 
39 static inline bool
pkgconf_version_is_separator(unsigned char ch)40 pkgconf_version_is_separator(unsigned char ch)
41 {
42 	return !isalnum(ch) && ch != '~';
43 }
44 
45 static const char *
pkgconf_version_skip_separators(const char * s)46 pkgconf_version_skip_separators(const char *s)
47 {
48 	while (*s && pkgconf_version_is_separator((unsigned char)*s))
49 		s++;
50 
51 	return s;
52 }
53 
54 static pkgconf_version_token_t
pkgconf_version_next_token(pkgconf_version_iter_t * it)55 pkgconf_version_next_token(pkgconf_version_iter_t *it)
56 {
57 	pkgconf_version_token_t tok;
58 	const char *s = pkgconf_version_skip_separators(it->cur);
59 
60 	tok.start = s;
61 	tok.end = s;
62 	tok.kind = PKGCONF_VERSION_TOKEN_END;
63 
64 	if (*s == '\0')
65 	{
66 		it->cur = s;
67 		return tok;
68 	}
69 
70 	if (*s == '~')
71 	{
72 		tok.kind = PKGCONF_VERSION_TOKEN_TILDE;
73 		tok.end = s + 1;
74 		it->cur = tok.end;
75 		return tok;
76 	}
77 
78 	if (isdigit((unsigned char)*s))
79 	{
80 		tok.kind = PKGCONF_VERSION_TOKEN_NUMERIC;
81 		while (*tok.end && isdigit((unsigned char)*tok.end))
82 			tok.end++;
83 		it->cur = tok.end;
84 		return tok;
85 	}
86 
87 	/*
88 	 * Having skipped separators and ruled out end-of-string, tilde and
89 	 * digits, the only remaining possibility is alpha: isalnum(c) is by
90 	 * definition isalpha(c) || isdigit(c).
91 	 */
92 	tok.kind = PKGCONF_VERSION_TOKEN_ALPHA;
93 	while (*tok.end && isalpha((unsigned char)*tok.end))
94 		tok.end++;
95 	it->cur = tok.end;
96 
97 	return tok;
98 }
99 
100 static int
pkgconf_version_compare_numeric(const pkgconf_version_token_t * a,const pkgconf_version_token_t * b)101 pkgconf_version_compare_numeric(const pkgconf_version_token_t *a, const pkgconf_version_token_t *b)
102 {
103 	const char *ap = a->start;
104 	const char *bp = b->start;
105 	size_t alen, blen;
106 	int ret;
107 
108 	while (ap < a->end && *ap == '0')
109 		ap++;
110 
111 	while (bp < b->end && *bp == '0')
112 		bp++;
113 
114 	alen = (size_t)(a->end - ap);
115 	blen = (size_t)(b->end - bp);
116 
117 	if (alen > blen)
118 		return 1;
119 	if (alen < blen)
120 		return -1;
121 
122 	if (alen == 0)
123 		return 0;
124 
125 	ret = strncmp(ap, bp, alen);
126 	if (ret < 0)
127 		return -1;
128 	if (ret > 0)
129 		return 1;
130 
131 	return 0;
132 }
133 
134 static int
pkgconf_version_compare_alpha(const pkgconf_version_token_t * a,const pkgconf_version_token_t * b)135 pkgconf_version_compare_alpha(const pkgconf_version_token_t *a, const pkgconf_version_token_t *b)
136 {
137 	size_t alen = (size_t)(a->end - a->start);
138 	size_t blen = (size_t)(b->end - b->start);
139 	size_t len = alen < blen ? alen : blen;
140 	int ret;
141 
142 	ret = strncmp(a->start, b->start, len);
143 	if (ret < 0)
144 		return -1;
145 	if (ret > 0)
146 		return 1;
147 
148 	if (alen < blen)
149 		return -1;
150 	if (alen > blen)
151 		return 1;
152 
153 	return 0;
154 }
155 
156 static int
pkgconf_version_compare_token(const pkgconf_version_token_t * a,const pkgconf_version_token_t * b)157 pkgconf_version_compare_token(const pkgconf_version_token_t *a, const pkgconf_version_token_t *b)
158 {
159 	if (a->kind == PKGCONF_VERSION_TOKEN_TILDE || b->kind == PKGCONF_VERSION_TOKEN_TILDE)
160 	{
161 		if (a->kind != PKGCONF_VERSION_TOKEN_TILDE)
162 			return 1;
163 		if (b->kind != PKGCONF_VERSION_TOKEN_TILDE)
164 			return -1;
165 
166 		return 0;
167 	}
168 
169 	if (a->kind == PKGCONF_VERSION_TOKEN_END || b->kind == PKGCONF_VERSION_TOKEN_END)
170 	{
171 		if (a->kind == PKGCONF_VERSION_TOKEN_END && b->kind == PKGCONF_VERSION_TOKEN_END)
172 			return 0;
173 		if (a->kind == PKGCONF_VERSION_TOKEN_END)
174 			return -1;
175 
176 		return 1;
177 	}
178 
179 	/* left-side is numeric, beats any right-side non-numeric */
180 	if (a->kind == PKGCONF_VERSION_TOKEN_NUMERIC)
181 	{
182 		if (b->kind != PKGCONF_VERSION_TOKEN_NUMERIC)
183 			return 1;
184 
185 		return pkgconf_version_compare_numeric(a, b);
186 	}
187 
188 	/* left-side is alpha, any right-side non-alpha wins */
189 	if (b->kind != PKGCONF_VERSION_TOKEN_ALPHA)
190 		return -1;
191 
192 	return pkgconf_version_compare_alpha(a, b);
193 }
194 
195 /*
196  * !doc
197  *
198  * .. c:function:: int pkgconf_compare_version(const char *a, const char *b)
199  *
200  *    Compare versions using RPM version comparison rules as described in the LSB.
201  *
202  *    :param char* a: The first version to compare in the pair.
203  *    :param char* b: The second version to compare in the pair.
204  *    :return: -1 if the first version is less than, 0 if both versions are equal, 1 if the second version is less than.
205  *    :rtype: int
206  */
207 int
pkgconf_compare_version(const char * a,const char * b)208 pkgconf_compare_version(const char *a, const char *b)
209 {
210 	pkgconf_version_iter_t ia, ib;
211 
212 	if (a == NULL)
213 		return -1;
214 	if (b == NULL)
215 		return 1;
216 
217 	if (!strcasecmp(a, b))
218 		return 0;
219 
220 	ia.cur = a;
221 	ib.cur = b;
222 
223 	for (;;)
224 	{
225 		pkgconf_version_token_t ta = pkgconf_version_next_token(&ia);
226 		pkgconf_version_token_t tb = pkgconf_version_next_token(&ib);
227 		int ret = pkgconf_version_compare_token(&ta, &tb);
228 
229 		if (ret != 0)
230 			return ret;
231 
232 		if (ta.kind == PKGCONF_VERSION_TOKEN_END &&
233 			tb.kind == PKGCONF_VERSION_TOKEN_END)
234 		{
235 			return 0;
236 		}
237 	}
238 }
239