/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: version.reef Authors: Chris Tusa License: 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