|
root / src / util / version.reef
version.reef Reef 317 lines 8.9 KB
  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