diff --git a/src/generators/bls.rs b/src/generators/bls.rs index 22fa869..d4e6929 100644 --- a/src/generators/bls.rs +++ b/src/generators/bls.rs @@ -2,6 +2,7 @@ use crate::context::SproutContext; use crate::entries::{BootableEntry, EntryDeclaration}; use crate::generators::bls::entry::BlsEntry; use crate::utils; +use crate::utils::vercmp; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; @@ -69,7 +70,12 @@ fn sort_entries(a: &(BlsEntry, BootableEntry), b: &(BlsEntry, BootableEntry)) -> let b_version = b_bls.version(); // Compare the version of both entries, sorting newer versions first. - match b_version.cmp(&a_version) { + match vercmp::compare_versions_optional( + a_version.as_deref(), + b_version.as_deref(), + ) + .reverse() + { // If both versions are equal, sort by file name in reverse order. Ordering::Equal => { // Grab the file name from both entries. @@ -77,7 +83,7 @@ fn sort_entries(a: &(BlsEntry, BootableEntry), b: &(BlsEntry, BootableEntry)) -> let b_name = b_boot.name(); // Compare the file names of both entries, sorting newer entries first. - b_name.cmp(a_name) + vercmp::compare_versions(a_name, b_name).reverse() } other => other, } diff --git a/src/utils.rs b/src/utils.rs index 24352d9..c2bd399 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -18,6 +18,9 @@ pub mod media_loader; /// Support code for EFI variables. pub mod variables; +/// Implements a version comparison algorithm according to the BLS specification. +pub mod vercmp; + /// Parses the input `path` as a [DevicePath]. /// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol. pub fn text_to_device_path(path: &str) -> Result { diff --git a/src/utils/vercmp.rs b/src/utils/vercmp.rs new file mode 100644 index 0000000..67c2aa1 --- /dev/null +++ b/src/utils/vercmp.rs @@ -0,0 +1,184 @@ +use std::cmp::Ordering; +use std::iter::Peekable; + +/// Handles single character advancement and comparison. +macro_rules! handle_single_char { + ($ca: expr, $cb:expr, $a_chars:expr, $b_chars:expr, $c:expr) => { + match ($ca == $c, $cb == $c) { + (true, false) => return Ordering::Less, + (false, true) => return Ordering::Greater, + (true, true) => { + $a_chars.next(); + $b_chars.next(); + continue; + } + _ => {} + } + }; +} + +/// Compares two strings using the BLS version comparison specification. +/// Handles optional values as well by comparing only if both are specified. +pub fn compare_versions_optional(a: Option<&str>, b: Option<&str>) -> Ordering { + match (a, b) { + // If both have values, compare them. + (Some(a), Some(b)) => compare_versions(a, b), + // If the second value is None, return that it is less than the first. + (Some(_a), None) => Ordering::Less, + // If the first value is None, return that it is greater than the second. + (None, Some(_b)) => Ordering::Greater, + // If both values are None, return that they are equal. + (None, None) => Ordering::Equal, + } +} + +/// Compares two strings using the BLS version comparison specification. +/// See: https://uapi-group.org/specifications/specs/version_format_specification/ +pub fn compare_versions(a: &str, b: &str) -> Ordering { + // Acquire a peekable iterator for each string. + let mut a_chars = a.chars().peekable(); + let mut b_chars = b.chars().peekable(); + + // Loop until we have reached the end of one of the strings. + loop { + // Skip invalid characters in both strings. + skip_invalid(&mut a_chars); + skip_invalid(&mut b_chars); + + // Check if either string has ended. + match (a_chars.peek(), b_chars.peek()) { + // No more characters in either string. + (None, None) => return Ordering::Equal, + // One string has ended, the other hasn't. + (None, Some(_)) => return Ordering::Less, + (Some(_), None) => return Ordering::Greater, + // Both strings have characters left. + (Some(&ca), Some(&cb)) => { + // Handle the ~ character. + handle_single_char!(ca, cb, a_chars, b_chars, '~'); + + // Handle '-' character. + handle_single_char!(ca, cb, a_chars, b_chars, '-'); + + // Handle the '^' character. + handle_single_char!(ca, cb, a_chars, b_chars, '^'); + + // Handle the '.' character. + handle_single_char!(ca, cb, a_chars, b_chars, '.'); + + // Handle digits with numerical comparison. + // We key off of the A character being a digit intentionally as we presume + // this indicates it will be the same at this position. + if ca.is_ascii_digit() || cb.is_ascii_digit() { + let result = compare_numeric(&mut a_chars, &mut b_chars); + if result != Ordering::Equal { + return result; + } + continue; + } + + // Handle letters with alphabetical comparison. + // We key off of the A character being alphabetical intentionally as we presume + // this indicates it will be the same at this position. + if ca.is_ascii_alphabetic() || cb.is_ascii_alphabetic() { + let result = compare_alphabetic(&mut a_chars, &mut b_chars); + if result != Ordering::Equal { + return result; + } + continue; + } + } + } + } +} + +/// Skips characters that are not in the valid character set. +fn skip_invalid>(iter: &mut Peekable) { + while let Some(&c) = iter.peek() { + if is_valid_char(c) { + break; + } + iter.next(); + } +} + +/// Checks if a character is in the valid character set for comparison. +fn is_valid_char(c: char) -> bool { + matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '.' | '~' | '^') +} + +/// Compares numerical prefixes by extracting numbers. +fn compare_numeric>( + iter_a: &mut Peekable, + iter_b: &mut Peekable, +) -> Ordering { + let num_a = extract_number(iter_a); + let num_b = extract_number(iter_b); + + num_a.cmp(&num_b) +} + +/// Extracts a number from the iterator, skipping leading zeros. +fn extract_number>(iter: &mut Peekable) -> u64 { + // Skip leading zeros + while let Some(&'0') = iter.peek() { + iter.next(); + } + + let mut num = 0u64; + while let Some(&c) = iter.peek() { + if c.is_ascii_digit() { + iter.next(); + num = num.saturating_mul(10).saturating_add(c as u64 - '0' as u64); + } else { + break; + } + } + + num +} + +/// Compares alphabetical prefixes +/// Capital letters compare lower than lowercase letters (B < a) +fn compare_alphabetic>( + iter_a: &mut Peekable, + iter_b: &mut Peekable, +) -> Ordering { + loop { + return match (iter_a.peek(), iter_b.peek()) { + (Some(&ca), Some(&cb)) if ca.is_ascii_alphabetic() && cb.is_ascii_alphabetic() => { + if ca == cb { + // Same character, we should continue. + iter_a.next(); + iter_b.next(); + continue; + } + + // Different characters found. + // All capital letters compare lower than lowercase letters. + match (ca.is_ascii_uppercase(), cb.is_ascii_uppercase()) { + (true, false) => Ordering::Less, // uppercase < lowercase + (false, true) => Ordering::Greater, // lowercase > uppercase + (true, true) => ca.cmp(&cb), // both are uppercase + (false, false) => ca.cmp(&cb), // both are lowercase + } + } + + (Some(&ca), Some(_)) if ca.is_ascii_alphabetic() => { + // a has letters, b doesn't + Ordering::Greater + } + + (Some(_), Some(&cb)) if cb.is_ascii_alphabetic() => { + // b has letters, a doesn't + Ordering::Less + } + + (Some(&ca), None) if ca.is_ascii_alphabetic() => Ordering::Greater, + + (None, Some(&cb)) if cb.is_ascii_alphabetic() => Ordering::Less, + + _ => Ordering::Equal, + }; + } +}