Skip to content

Commit f6441b5

Browse files
committed
feat(bls): add version comparison to ensure entries are always sorted properly
1 parent 03d0e40 commit f6441b5

File tree

3 files changed

+195
-2
lines changed

3 files changed

+195
-2
lines changed

src/generators/bls.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::context::SproutContext;
22
use crate::entries::{BootableEntry, EntryDeclaration};
33
use crate::generators::bls::entry::BlsEntry;
44
use crate::utils;
5+
use crate::utils::vercmp;
56
use anyhow::{Context, Result};
67
use serde::{Deserialize, Serialize};
78
use std::cmp::Ordering;
@@ -69,15 +70,20 @@ fn sort_entries(a: &(BlsEntry, BootableEntry), b: &(BlsEntry, BootableEntry)) ->
6970
let b_version = b_bls.version();
7071

7172
// Compare the version of both entries, sorting newer versions first.
72-
match b_version.cmp(&a_version) {
73+
match vercmp::compare_versions_optional(
74+
a_version.as_deref(),
75+
b_version.as_deref(),
76+
)
77+
.reverse()
78+
{
7379
// If both versions are equal, sort by file name in reverse order.
7480
Ordering::Equal => {
7581
// Grab the file name from both entries.
7682
let a_name = a_boot.name();
7783
let b_name = b_boot.name();
7884

7985
// Compare the file names of both entries, sorting newer entries first.
80-
b_name.cmp(a_name)
86+
vercmp::compare_versions(a_name, b_name).reverse()
8187
}
8288
other => other,
8389
}

src/utils.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ pub mod media_loader;
1818
/// Support code for EFI variables.
1919
pub mod variables;
2020

21+
/// Implements a version comparison algorithm according to the BLS specification.
22+
pub mod vercmp;
23+
2124
/// Parses the input `path` as a [DevicePath].
2225
/// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol.
2326
pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> {

src/utils/vercmp.rs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
use std::cmp::Ordering;
2+
use std::iter::Peekable;
3+
4+
/// Handles single character advancement and comparison.
5+
macro_rules! handle_single_char {
6+
($ca: expr, $cb:expr, $a_chars:expr, $b_chars:expr, $c:expr) => {
7+
match ($ca == $c, $cb == $c) {
8+
(true, false) => return Ordering::Less,
9+
(false, true) => return Ordering::Greater,
10+
(true, true) => {
11+
$a_chars.next();
12+
$b_chars.next();
13+
continue;
14+
}
15+
_ => {}
16+
}
17+
};
18+
}
19+
20+
/// Compares two strings using the BLS version comparison specification.
21+
/// Handles optional values as well by comparing only if both are specified.
22+
pub fn compare_versions_optional(a: Option<&str>, b: Option<&str>) -> Ordering {
23+
match (a, b) {
24+
// If both have values, compare them.
25+
(Some(a), Some(b)) => compare_versions(a, b),
26+
// If the second value is None, return that it is less than the first.
27+
(Some(_a), None) => Ordering::Less,
28+
// If the first value is None, return that it is greater than the second.
29+
(None, Some(_b)) => Ordering::Greater,
30+
// If both values are None, return that they are equal.
31+
(None, None) => Ordering::Equal,
32+
}
33+
}
34+
35+
/// Compares two strings using the BLS version comparison specification.
36+
/// See: https://uapi-group.org/specifications/specs/version_format_specification/
37+
pub fn compare_versions(a: &str, b: &str) -> Ordering {
38+
// Acquire a peekable iterator for each string.
39+
let mut a_chars = a.chars().peekable();
40+
let mut b_chars = b.chars().peekable();
41+
42+
// Loop until we have reached the end of one of the strings.
43+
loop {
44+
// Skip invalid characters in both strings.
45+
skip_invalid(&mut a_chars);
46+
skip_invalid(&mut b_chars);
47+
48+
// Check if either string has ended.
49+
match (a_chars.peek(), b_chars.peek()) {
50+
// No more characters in either string.
51+
(None, None) => return Ordering::Equal,
52+
// One string has ended, the other hasn't.
53+
(None, Some(_)) => return Ordering::Less,
54+
(Some(_), None) => return Ordering::Greater,
55+
// Both strings have characters left.
56+
(Some(&ca), Some(&cb)) => {
57+
// Handle the ~ character.
58+
handle_single_char!(ca, cb, a_chars, b_chars, '~');
59+
60+
// Handle '-' character.
61+
handle_single_char!(ca, cb, a_chars, b_chars, '-');
62+
63+
// Handle the '^' character.
64+
handle_single_char!(ca, cb, a_chars, b_chars, '^');
65+
66+
// Handle the '.' character.
67+
handle_single_char!(ca, cb, a_chars, b_chars, '.');
68+
69+
// Handle digits with numerical comparison.
70+
// We key off of the A character being a digit intentionally as we presume
71+
// this indicates it will be the same at this position.
72+
if ca.is_ascii_digit() || cb.is_ascii_digit() {
73+
let result = compare_numeric(&mut a_chars, &mut b_chars);
74+
if result != Ordering::Equal {
75+
return result;
76+
}
77+
continue;
78+
}
79+
80+
// Handle letters with alphabetical comparison.
81+
// We key off of the A character being alphabetical intentionally as we presume
82+
// this indicates it will be the same at this position.
83+
if ca.is_ascii_alphabetic() || cb.is_ascii_alphabetic() {
84+
let result = compare_alphabetic(&mut a_chars, &mut b_chars);
85+
if result != Ordering::Equal {
86+
return result;
87+
}
88+
continue;
89+
}
90+
}
91+
}
92+
}
93+
}
94+
95+
/// Skips characters that are not in the valid character set.
96+
fn skip_invalid<I: Iterator<Item = char>>(iter: &mut Peekable<I>) {
97+
while let Some(&c) = iter.peek() {
98+
if is_valid_char(c) {
99+
break;
100+
}
101+
iter.next();
102+
}
103+
}
104+
105+
/// Checks if a character is in the valid character set for comparison.
106+
fn is_valid_char(c: char) -> bool {
107+
matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '.' | '~' | '^')
108+
}
109+
110+
/// Compares numerical prefixes by extracting numbers.
111+
fn compare_numeric<I: Iterator<Item = char>>(
112+
iter_a: &mut Peekable<I>,
113+
iter_b: &mut Peekable<I>,
114+
) -> Ordering {
115+
let num_a = extract_number(iter_a);
116+
let num_b = extract_number(iter_b);
117+
118+
num_a.cmp(&num_b)
119+
}
120+
121+
/// Extracts a number from the iterator, skipping leading zeros.
122+
fn extract_number<I: Iterator<Item = char>>(iter: &mut Peekable<I>) -> u64 {
123+
// Skip leading zeros
124+
while let Some(&'0') = iter.peek() {
125+
iter.next();
126+
}
127+
128+
let mut num = 0u64;
129+
while let Some(&c) = iter.peek() {
130+
if c.is_ascii_digit() {
131+
iter.next();
132+
num = num.saturating_mul(10).saturating_add(c as u64 - '0' as u64);
133+
} else {
134+
break;
135+
}
136+
}
137+
138+
num
139+
}
140+
141+
/// Compares alphabetical prefixes
142+
/// Capital letters compare lower than lowercase letters (B < a)
143+
fn compare_alphabetic<I: Iterator<Item = char>>(
144+
iter_a: &mut Peekable<I>,
145+
iter_b: &mut Peekable<I>,
146+
) -> Ordering {
147+
loop {
148+
return match (iter_a.peek(), iter_b.peek()) {
149+
(Some(&ca), Some(&cb)) if ca.is_ascii_alphabetic() && cb.is_ascii_alphabetic() => {
150+
if ca == cb {
151+
// Same character, we should continue.
152+
iter_a.next();
153+
iter_b.next();
154+
continue;
155+
}
156+
157+
// Different characters found.
158+
// All capital letters compare lower than lowercase letters.
159+
match (ca.is_ascii_uppercase(), cb.is_ascii_uppercase()) {
160+
(true, false) => Ordering::Less, // uppercase < lowercase
161+
(false, true) => Ordering::Greater, // lowercase > uppercase
162+
(true, true) => ca.cmp(&cb), // both are uppercase
163+
(false, false) => ca.cmp(&cb), // both are lowercase
164+
}
165+
}
166+
167+
(Some(&ca), Some(_)) if ca.is_ascii_alphabetic() => {
168+
// a has letters, b doesn't
169+
Ordering::Greater
170+
}
171+
172+
(Some(_), Some(&cb)) if cb.is_ascii_alphabetic() => {
173+
// b has letters, a doesn't
174+
Ordering::Less
175+
}
176+
177+
(Some(&ca), None) if ca.is_ascii_alphabetic() => Ordering::Greater,
178+
179+
(None, Some(&cb)) if cb.is_ascii_alphabetic() => Ordering::Less,
180+
181+
_ => Ordering::Equal,
182+
};
183+
}
184+
}

0 commit comments

Comments
 (0)