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
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
|
/******************************************************************************
__ ____ __
/ / ___ ____ _/ __/_____________ _/ /__
/ / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
/ /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/
/_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
(C)opyright 2025, Leafscale, LLC - https://www.leafscale.com
Project: Zygaena
Filename: version.reef
Authors: Chris Tusa <chris.tusa@leafscale.com>
License: <see LICENSE file included with this source code>
Description: Semantic version parsing and comparison utilities
******************************************************************************/
module util.version
import core.str
export
// Version constraint parsing
type DepConstraint
// Compare two version strings semantically
// Returns: -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
fn compare_versions(v1: string, v2: string): int
// Check if v1 is newer than v2
fn is_newer(v1: string, v2: string): bool
// Parse a dependency string with optional version constraint
// e.g., "libfoo>=1.0" -> { name: "libfoo", op: ">=", version: "1.0" }
fn parse_dep_constraint(dep: string): DepConstraint
// Check if an installed version satisfies a version constraint
fn check_version_constraint(installed_version: string, op: string, required_version: string): bool
// Accessor functions for DepConstraint
fn constraint_name(c: DepConstraint): string
fn constraint_op(c: DepConstraint): string
fn constraint_version(c: DepConstraint): string
fn has_constraint(c: DepConstraint): bool
// Extract version from package filename
// e.g., "reef-0.1.10-1.pkg.tar.xz" -> "0.1.10"
fn extract_version_from_filename(filename: string, pkg_name: string): string
// Extract release number from package filename
// e.g., "reef-0.1.10-1.pkg.tar.xz" -> 1
fn extract_release_from_filename(filename: string): int
end export
// Parsed dependency constraint
type DepConstraint = struct
name: string
op: string
version: string
end DepConstraint
// Compare two version strings semantically
// Handles versions like "0.1.10" vs "0.1.7" correctly (0.1.10 > 0.1.7)
fn compare_versions(v1: string, v2: string): int
// Parse version components
mut v1_parts: [int] = new [int](10)
mut v2_parts: [int] = new [int](10)
let v1_count = parse_version(v1, v1_parts, 10)
let v2_count = parse_version(v2, v2_parts, 10)
// Compare each component
let max_parts = max_int(v1_count, v2_count)
mut i = 0
while i < max_parts
let p1 = get_part(v1_parts, v1_count, i)
let p2 = get_part(v2_parts, v2_count, i)
if p1 > p2
return 1
elif p1 < p2
return -1
end if
i = i + 1
end while
return 0
end compare_versions
// Check if v1 is newer than v2
fn is_newer(v1: string, v2: string): bool
return compare_versions(v1, v2) > 0
end is_newer
// Parse version string into integer components
// "0.1.10" -> [0, 1, 10]
fn parse_version(version: string, parts: [int], max_parts: int): int
mut count = 0
mut current = 0
mut in_number = false
mut i = 0
while i < str.length(version) and count < max_parts
let c = str.char_at(version, i)
if is_digit(c)
current = current * 10 + char_to_digit(c)
in_number = true
elif c == '.' or c == '-'
if in_number
parts[count] = current
count = count + 1
current = 0
in_number = false
end if
else
// Skip non-numeric characters (like 'alpha', 'beta', 'rc')
if in_number
parts[count] = current
count = count + 1
current = 0
in_number = false
end if
end if
i = i + 1
end while
// Don't forget the last number
if in_number and count < max_parts
parts[count] = current
count = count + 1
end if
return count
end parse_version
// Extract version from package filename
// e.g., "reef-0.1.10-1.pkg.tar.xz" with pkg_name "reef" -> "0.1.10"
fn extract_version_from_filename(filename: string, pkg_name: string): string
// Remove package name prefix and dash
let prefix_len = str.length(pkg_name) + 1
if str.length(filename) <= prefix_len
return ""
end if
// Get the part after "pkgname-"
let rest = str.substring(filename, prefix_len, str.length(filename) - prefix_len)
// Find the last dash before .pkg.tar.xz (that's the release separator)
// e.g., "0.1.10-1.pkg.tar.xz" -> version is "0.1.10"
let suffix = ".pkg.tar.xz"
let suffix_len = str.length(suffix)
if not str.ends_with(rest, suffix)
return ""
end if
// Remove suffix: "0.1.10-1.pkg.tar.xz" -> "0.1.10-1"
let ver_rel = str.substring(rest, 0, str.length(rest) - suffix_len)
// Find last dash (release separator)
let last_dash = str.last_index_of_char(ver_rel, '-')
if last_dash < 0
return ver_rel
end if
// Return version part: "0.1.10-1" -> "0.1.10"
return str.substring(ver_rel, 0, last_dash)
end extract_version_from_filename
// Extract release number from package filename
// e.g., "reef-0.1.10-1.pkg.tar.xz" -> 1
fn extract_release_from_filename(filename: string): int
let suffix = ".pkg.tar.xz"
if not str.ends_with(filename, suffix)
return 1
end if
// Remove suffix
let without_suffix = str.substring(filename, 0, str.length(filename) - str.length(suffix))
// Find last dash
let last_dash = str.last_index_of_char(without_suffix, '-')
if last_dash < 0
return 1
end if
// Extract release number
let release_str = str.substring(without_suffix, last_dash + 1, str.length(without_suffix) - last_dash - 1)
return parse_int(release_str)
end extract_release_from_filename
// Helper: check if character is a digit
fn is_digit(c: char): bool
return c >= '0' and c <= '9'
end is_digit
// Helper: convert digit character to integer
fn char_to_digit(c: char): int
return c - '0'
end char_to_digit
// Helper: get part at index, or 0 if out of bounds
fn get_part(parts: [int], count: int, idx: int): int
if idx < count
return parts[idx]
end if
return 0
end get_part
// Helper: max of two integers
fn max_int(a: int, b: int): int
if a > b
return a
end if
return b
end max_int
// Helper: parse string to int
fn parse_int(s: string): int
mut result = 0
mut i = 0
while i < str.length(s)
let c = str.char_at(s, i)
if is_digit(c)
result = result * 10 + char_to_digit(c)
else
break
end if
i = i + 1
end while
return result
end parse_int
// Parse a dependency string into name, operator, and version
// Handles: "libfoo", "libfoo>=1.0", "libfoo<=2.0", "libfoo>1.0", "libfoo<2.0", "libfoo=1.0"
fn parse_dep_constraint(dep: string): DepConstraint
let len = str.length(dep)
mut i = 0
while i < len
let c = str.char_at(dep, i)
if c == '>' or c == '<' or c == '='
let name = str.substring(dep, 0, i)
// Check for two-char operators (>=, <=)
if i + 1 < len
let next = str.char_at(dep, i + 1)
if next == '='
let op = str.substring(dep, i, 2)
let ver = str.substring(dep, i + 2, len - i - 2)
return DepConstraint{ name: name, op: op, version: ver }
end if
end if
// Single char operator (>, <, =)
let op = str.substring(dep, i, 1)
let ver = str.substring(dep, i + 1, len - i - 1)
return DepConstraint{ name: name, op: op, version: ver }
end if
i = i + 1
end while
// No constraint
return DepConstraint{ name: dep, op: "", version: "" }
end parse_dep_constraint
// Check if an installed version satisfies a version constraint
fn check_version_constraint(installed_version: string, op: string, required_version: string): bool
if str.length(op) == 0
return true
end if
let cmp = compare_versions(installed_version, required_version)
if str.equals(op, ">=")
return cmp >= 0
elif str.equals(op, "<=")
return cmp <= 0
elif str.equals(op, ">")
return cmp > 0
elif str.equals(op, "<")
return cmp < 0
elif str.equals(op, "=")
return cmp == 0
end if
return true
end check_version_constraint
// Accessor functions
fn constraint_name(c: DepConstraint): string
return c.name
end constraint_name
fn constraint_op(c: DepConstraint): string
return c.op
end constraint_op
fn constraint_version(c: DepConstraint): string
return c.version
end constraint_version
fn has_constraint(c: DepConstraint): bool
return str.length(c.op) > 0
end has_constraint
end module
|