From 376dda2836a14c94976c9f6fa882169df49d8113 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 14 Aug 2025 11:38:54 +0200 Subject: [PATCH 1/4] Rename grub feature to composefs-backend This should make it easier to incrementally merge changes here to main. Signed-off-by: Colin Walters --- crates/lib/Cargo.toml | 6 +++--- crates/lib/src/lib.rs | 3 ++- crates/lib/src/parsers/grub_menuconfig.rs | 8 +------- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 1053e4cee..7e6051244 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -70,13 +70,13 @@ similar-asserts = { workspace = true } static_assertions = { workspace = true } [features] -default = ["install-to-disk", "grub"] +default = ["install-to-disk"] # This feature enables `bootc install to-disk`, which is considered just a "demo" # or reference installer; we expect most nontrivial use cases to be using # `bootc install to-filesystem`. install-to-disk = [] -# Enable direct support for the GRUB bootloader -grub = [] +# Enable support for the composefs native backend +composefs-backend = [] # This featuares enables `bootc internals publish-rhsm-facts` to integrate with # Red Hat Subscription Manager rhsm = [] diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index a60d0bc30..4b40a9ecd 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -36,7 +36,8 @@ mod containerenv; mod install; mod kernel_cmdline; -#[cfg(feature = "grub")] +#[cfg(feature = "composefs-backend")] +#[allow(dead_code)] pub(crate) mod parsers; #[cfg(feature = "rhsm")] mod rhsm; diff --git a/crates/lib/src/parsers/grub_menuconfig.rs b/crates/lib/src/parsers/grub_menuconfig.rs index 688a07af0..146903ef9 100644 --- a/crates/lib/src/parsers/grub_menuconfig.rs +++ b/crates/lib/src/parsers/grub_menuconfig.rs @@ -71,7 +71,6 @@ impl<'a> From> for MenuentryBody<'a> { /// A complete GRUB menuentry with title and body commands. #[derive(Debug, PartialEq, Eq)] -#[allow(dead_code)] pub(crate) struct MenuEntry<'a> { /// Display title (supports escaped quotes) pub(crate) title: &'a str, @@ -88,8 +87,7 @@ impl<'a> Display for MenuEntry<'a> { } /// Parser that takes content until balanced brackets, handling nested brackets and escapes. -#[allow(dead_code)] -pub fn take_until_balanced_allow_nested( +fn take_until_balanced_allow_nested( opening_bracket: char, closing_bracket: char, ) -> impl Fn(&str) -> IResult<&str, &str> { @@ -141,7 +139,6 @@ pub fn take_until_balanced_allow_nested( } /// Parses a single menuentry with title and body commands. -#[allow(dead_code)] fn parse_menuentry(input: &str) -> IResult<&str, MenuEntry<'_>> { let (input, _) = tag("menuentry").parse(input)?; @@ -190,14 +187,12 @@ fn parse_menuentry(input: &str) -> IResult<&str, MenuEntry<'_>> { } /// Skips content until finding "menuentry" keyword or end of input. -#[allow(dead_code)] fn skip_to_menuentry(input: &str) -> IResult<&str, ()> { let (input, _) = take_until("menuentry")(input)?; Ok((input, ())) } /// Parses all menuentries from a GRUB configuration file. -#[allow(dead_code)] fn parse_all(input: &str) -> IResult<&str, Vec>> { let mut remaining = input; let mut entries = Vec::new(); @@ -229,7 +224,6 @@ fn parse_all(input: &str) -> IResult<&str, Vec>> { } /// Main entry point for parsing GRUB menuentry files. -#[allow(dead_code)] pub(crate) fn parse_grub_menuentry_file(contents: &str) -> anyhow::Result>> { let (_, entries) = parse_all(&contents) .map_err(|e| anyhow::anyhow!("Failed to parse GRUB menuentries: {e}"))?; From 1a6e914a6f6475f6d703b2699803216dac4475f5 Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Tue, 29 Jul 2025 11:37:26 +0530 Subject: [PATCH 2/4] parser/bls: New file Signed-off-by: Johan-Liebert1 Signed-off-by: Colin Walters --- crates/lib/src/parsers/bls_config.rs | 205 +++++++++++++++++++++++++++ crates/lib/src/parsers/mod.rs | 1 + 2 files changed, 206 insertions(+) create mode 100644 crates/lib/src/parsers/bls_config.rs diff --git a/crates/lib/src/parsers/bls_config.rs b/crates/lib/src/parsers/bls_config.rs new file mode 100644 index 000000000..5bcc47a6d --- /dev/null +++ b/crates/lib/src/parsers/bls_config.rs @@ -0,0 +1,205 @@ +//! See +//! +//! This module parses the config files for the spec. + +use anyhow::Result; +use serde::de::Error; +use serde::{Deserialize, Deserializer}; +use std::collections::HashMap; +use std::fmt::Display; + +#[derive(Debug, Deserialize, Eq)] +pub(crate) struct BLSConfig { + pub(crate) title: Option, + #[serde(deserialize_with = "deserialize_version")] + pub(crate) version: u32, + pub(crate) linux: String, + pub(crate) initrd: String, + pub(crate) options: String, + + #[serde(flatten)] + pub(crate) extra: HashMap, +} + +impl PartialEq for BLSConfig { + fn eq(&self, other: &Self) -> bool { + self.version == other.version + } +} + +impl PartialOrd for BLSConfig { + fn partial_cmp(&self, other: &Self) -> Option { + self.version.partial_cmp(&other.version) + } +} + +impl Ord for BLSConfig { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.version.cmp(&other.version) + } +} + +impl Display for BLSConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(title) = &self.title { + writeln!(f, "title {}", title)?; + } + + writeln!(f, "version {}", self.version)?; + writeln!(f, "linux {}", self.linux)?; + writeln!(f, "initrd {}", self.initrd)?; + writeln!(f, "options {}", self.options)?; + + for (key, value) in &self.extra { + writeln!(f, "{} {}", key, value)?; + } + + Ok(()) + } +} + +fn deserialize_version<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s: Option = Option::deserialize(deserializer)?; + + match s { + Some(s) => Ok(s.parse::().map_err(D::Error::custom)?), + None => Err(D::Error::custom("Version not found")), + } +} + +pub(crate) fn parse_bls_config(input: &str) -> Result { + let mut map = HashMap::new(); + + for line in input.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + + if let Some((key, value)) = line.split_once(' ') { + map.insert(key.to_string(), value.trim().to_string()); + } + } + + let value = serde_json::to_value(map)?; + let parsed: BLSConfig = serde_json::from_value(value)?; + + Ok(parsed) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_valid_bls_config() -> Result<()> { + let input = r#" + title Fedora 42.20250623.3.1 (CoreOS) + version 2 + linux /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10 + initrd /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img + options root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6 + custom1 value1 + custom2 value2 + "#; + + let config = parse_bls_config(input)?; + + assert_eq!( + config.title, + Some("Fedora 42.20250623.3.1 (CoreOS)".to_string()) + ); + assert_eq!(config.version, 2); + assert_eq!(config.linux, "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10"); + assert_eq!(config.initrd, "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img"); + assert_eq!(config.options, "root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6"); + assert_eq!(config.extra.get("custom1"), Some(&"value1".to_string())); + assert_eq!(config.extra.get("custom2"), Some(&"value2".to_string())); + + Ok(()) + } + + #[test] + fn test_parse_missing_version() { + let input = r#" + title Fedora + linux /vmlinuz + initrd /initramfs.img + options root=UUID=xyz ro quiet + "#; + + let parsed = parse_bls_config(input); + assert!(parsed.is_err()); + } + + #[test] + fn test_parse_invalid_version_format() { + let input = r#" + title Fedora + version not_an_int + linux /vmlinuz + initrd /initramfs.img + options root=UUID=abc composefs=some-uuid + "#; + + let parsed = parse_bls_config(input); + assert!(parsed.is_err()); + } + + #[test] + fn test_display_output() -> Result<()> { + let input = r#" + title Test OS + version 10 + linux /boot/vmlinuz + initrd /boot/initrd.img + options root=UUID=abc composefs=some-uuid + foo bar + "#; + + let config = parse_bls_config(input)?; + let output = format!("{}", config); + let mut output_lines = output.lines(); + + assert_eq!(output_lines.next().unwrap(), "title Test OS"); + assert_eq!(output_lines.next().unwrap(), "version 10"); + assert_eq!(output_lines.next().unwrap(), "linux /boot/vmlinuz"); + assert_eq!(output_lines.next().unwrap(), "initrd /boot/initrd.img"); + assert_eq!( + output_lines.next().unwrap(), + "options root=UUID=abc composefs=some-uuid" + ); + assert_eq!(output_lines.next().unwrap(), "foo bar"); + + Ok(()) + } + + #[test] + fn test_ordering() -> Result<()> { + let config1 = parse_bls_config( + r#" + title Entry 1 + version 3 + linux /vmlinuz-3 + initrd /initrd-3 + options opt1 + "#, + )?; + + let config2 = parse_bls_config( + r#" + title Entry 2 + version 5 + linux /vmlinuz-5 + initrd /initrd-5 + options opt2 + "#, + )?; + + assert!(config1 < config2); + Ok(()) + } +} diff --git a/crates/lib/src/parsers/mod.rs b/crates/lib/src/parsers/mod.rs index ca3d0453a..e3640c8ef 100644 --- a/crates/lib/src/parsers/mod.rs +++ b/crates/lib/src/parsers/mod.rs @@ -1 +1,2 @@ +pub(crate) mod bls_config; pub(crate) mod grub_menuconfig; From cbb6eaa70f5b22f52fc2b4ec86d28117505dc208 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 14 Aug 2025 12:14:38 +0200 Subject: [PATCH 3/4] bls_config: Rework to be more spec compliant - Handle multiple initrd entries. - Add support for machine-id and sort-key. To do this we can't just map the keys into JSON because we need to handle multiple values. Switch to a manual parser. Assisted-by: Gemini CLI+gemini-2.5-pro Signed-off-by: Colin Walters --- crates/lib/src/parsers/bls_config.rs | 284 ++++++++++++++++++++++----- 1 file changed, 238 insertions(+), 46 deletions(-) diff --git a/crates/lib/src/parsers/bls_config.rs b/crates/lib/src/parsers/bls_config.rs index 5bcc47a6d..a6fcbd781 100644 --- a/crates/lib/src/parsers/bls_config.rs +++ b/crates/lib/src/parsers/bls_config.rs @@ -2,40 +2,69 @@ //! //! This module parses the config files for the spec. -use anyhow::Result; -use serde::de::Error; -use serde::{Deserialize, Deserializer}; +use anyhow::{anyhow, Result}; use std::collections::HashMap; use std::fmt::Display; -#[derive(Debug, Deserialize, Eq)] +/// Represents a single Boot Loader Specification config file. +/// +/// The boot loader should present the available boot menu entries to the user in a sorted list. +/// The list should be sorted by the `sort-key` field, if it exists, otherwise by the `machine-id` field. +/// If multiple entries have the same `sort-key` (or `machine-id`), they should be sorted by the `version` field in descending order. +#[derive(Debug, Eq, PartialEq)] +#[non_exhaustive] pub(crate) struct BLSConfig { + /// The title of the boot entry, to be displayed in the boot menu. pub(crate) title: Option, - #[serde(deserialize_with = "deserialize_version")] - pub(crate) version: u32, + /// The version of the boot entry. + /// See + pub(crate) version: String, + /// The path to the linux kernel to boot. pub(crate) linux: String, - pub(crate) initrd: String, - pub(crate) options: String, - - #[serde(flatten)] + /// The paths to the initrd images. + pub(crate) initrd: Vec, + /// Kernel command line options. + pub(crate) options: Option, + /// The machine ID of the OS. + pub(crate) machine_id: Option, + /// The sort key for the boot menu. + pub(crate) sort_key: Option, + + /// Any extra fields not defined in the spec. pub(crate) extra: HashMap, } -impl PartialEq for BLSConfig { - fn eq(&self, other: &Self) -> bool { - self.version == other.version - } -} - impl PartialOrd for BLSConfig { fn partial_cmp(&self, other: &Self) -> Option { - self.version.partial_cmp(&other.version) + Some(self.cmp(other)) } } impl Ord for BLSConfig { + /// This implements the sorting logic from the Boot Loader Specification. + /// + /// The list should be sorted by the `sort-key` field, if it exists, otherwise by the `machine-id` field. + /// If multiple entries have the same `sort-key` (or `machine-id`), they should be sorted by the `version` field in descending order. fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.version.cmp(&other.version) + // If both configs have a sort key, compare them. + if let (Some(key1), Some(key2)) = (&self.sort_key, &other.sort_key) { + let ord = key1.cmp(key2); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + + // If both configs have a machine ID, compare them. + if let (Some(id1), Some(id2)) = (&self.machine_id, &other.machine_id) { + let ord = id1.cmp(id2); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + + // Finally, sort by version in descending order. + // FIXME: This should use + self.version.cmp(&other.version).reverse() } } @@ -47,8 +76,18 @@ impl Display for BLSConfig { writeln!(f, "version {}", self.version)?; writeln!(f, "linux {}", self.linux)?; - writeln!(f, "initrd {}", self.initrd)?; - writeln!(f, "options {}", self.options)?; + for initrd in self.initrd.iter() { + writeln!(f, "initrd {}", initrd)?; + } + if let Some(options) = self.options.as_deref() { + writeln!(f, "options {}", options)?; + } + if let Some(machine_id) = self.machine_id.as_deref() { + writeln!(f, "machine-id {}", machine_id)?; + } + if let Some(sort_key) = self.sort_key.as_deref() { + writeln!(f, "sort-key {}", sort_key)?; + } for (key, value) in &self.extra { writeln!(f, "{} {}", key, value)?; @@ -58,20 +97,15 @@ impl Display for BLSConfig { } } -fn deserialize_version<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let s: Option = Option::deserialize(deserializer)?; - - match s { - Some(s) => Ok(s.parse::().map_err(D::Error::custom)?), - None => Err(D::Error::custom("Version not found")), - } -} - pub(crate) fn parse_bls_config(input: &str) -> Result { - let mut map = HashMap::new(); + let mut title = None; + let mut version = None; + let mut linux = None; + let mut initrd = Vec::new(); + let mut options = None; + let mut machine_id = None; + let mut sort_key = None; + let mut extra = HashMap::new(); for line in input.lines() { let line = line.trim(); @@ -80,14 +114,35 @@ pub(crate) fn parse_bls_config(input: &str) -> Result { } if let Some((key, value)) = line.split_once(' ') { - map.insert(key.to_string(), value.trim().to_string()); + let value = value.trim().to_string(); + match key { + "title" => title = Some(value), + "version" => version = Some(value), + "linux" => linux = Some(value), + "initrd" => initrd.push(value), + "options" => options = Some(value), + "machine-id" => machine_id = Some(value), + "sort-key" => sort_key = Some(value), + _ => { + extra.insert(key.to_string(), value); + } + } } } - let value = serde_json::to_value(map)?; - let parsed: BLSConfig = serde_json::from_value(value)?; - - Ok(parsed) + let linux = linux.ok_or_else(|| anyhow!("Missing 'linux' value"))?; + let version = version.ok_or_else(|| anyhow!("Missing 'version' value"))?; + + Ok(BLSConfig { + title, + version, + linux, + initrd, + options, + machine_id, + sort_key, + extra, + }) } #[cfg(test)] @@ -112,16 +167,37 @@ mod tests { config.title, Some("Fedora 42.20250623.3.1 (CoreOS)".to_string()) ); - assert_eq!(config.version, 2); + assert_eq!(config.version, "2"); assert_eq!(config.linux, "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10"); - assert_eq!(config.initrd, "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img"); - assert_eq!(config.options, "root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6"); + assert_eq!(config.initrd, vec!["/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img"]); + assert_eq!(config.options, Some("root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6".to_string())); assert_eq!(config.extra.get("custom1"), Some(&"value1".to_string())); assert_eq!(config.extra.get("custom2"), Some(&"value2".to_string())); Ok(()) } + #[test] + fn test_parse_multiple_initrd() -> Result<()> { + let input = r#" + title Fedora 42.20250623.3.1 (CoreOS) + version 2 + linux /boot/vmlinuz + initrd /boot/initramfs-1.img + initrd /boot/initramfs-2.img + options root=UUID=abc123 rw + "#; + + let config = parse_bls_config(input)?; + + assert_eq!( + config.initrd, + vec!["/boot/initramfs-1.img", "/boot/initramfs-2.img"] + ); + + Ok(()) + } + #[test] fn test_parse_missing_version() { let input = r#" @@ -136,13 +212,12 @@ mod tests { } #[test] - fn test_parse_invalid_version_format() { + fn test_parse_missing_linux() { let input = r#" title Fedora - version not_an_int - linux /vmlinuz + version 1 initrd /initramfs.img - options root=UUID=abc composefs=some-uuid + options root=UUID=xyz ro quiet "#; let parsed = parse_bls_config(input); @@ -156,6 +231,7 @@ mod tests { version 10 linux /boot/vmlinuz initrd /boot/initrd.img + initrd /boot/initrd-extra.img options root=UUID=abc composefs=some-uuid foo bar "#; @@ -168,6 +244,10 @@ mod tests { assert_eq!(output_lines.next().unwrap(), "version 10"); assert_eq!(output_lines.next().unwrap(), "linux /boot/vmlinuz"); assert_eq!(output_lines.next().unwrap(), "initrd /boot/initrd.img"); + assert_eq!( + output_lines.next().unwrap(), + "initrd /boot/initrd-extra.img" + ); assert_eq!( output_lines.next().unwrap(), "options root=UUID=abc composefs=some-uuid" @@ -178,11 +258,38 @@ mod tests { } #[test] - fn test_ordering() -> Result<()> { + fn test_ordering_by_version() -> Result<()> { + let config1 = parse_bls_config( + r#" + title Entry 1 + version 3 + linux /vmlinuz-3 + initrd /initrd-3 + options opt1 + "#, + )?; + + let config2 = parse_bls_config( + r#" + title Entry 2 + version 5 + linux /vmlinuz-5 + initrd /initrd-5 + options opt2 + "#, + )?; + + assert!(config1 > config2); + Ok(()) + } + + #[test] + fn test_ordering_by_sort_key() -> Result<()> { let config1 = parse_bls_config( r#" title Entry 1 version 3 + sort-key a linux /vmlinuz-3 initrd /initrd-3 options opt1 @@ -193,6 +300,7 @@ mod tests { r#" title Entry 2 version 5 + sort-key b linux /vmlinuz-5 initrd /initrd-5 options opt2 @@ -202,4 +310,88 @@ mod tests { assert!(config1 < config2); Ok(()) } + + #[test] + fn test_ordering_by_sort_key_and_version() -> Result<()> { + let config1 = parse_bls_config( + r#" + title Entry 1 + version 3 + sort-key a + linux /vmlinuz-3 + initrd /initrd-3 + options opt1 + "#, + )?; + + let config2 = parse_bls_config( + r#" + title Entry 2 + version 5 + sort-key a + linux /vmlinuz-5 + initrd /initrd-5 + options opt2 + "#, + )?; + + assert!(config1 > config2); + Ok(()) + } + + #[test] + fn test_ordering_by_machine_id() -> Result<()> { + let config1 = parse_bls_config( + r#" + title Entry 1 + version 3 + machine-id a + linux /vmlinuz-3 + initrd /initrd-3 + options opt1 + "#, + )?; + + let config2 = parse_bls_config( + r#" + title Entry 2 + version 5 + machine-id b + linux /vmlinuz-5 + initrd /initrd-5 + options opt2 + "#, + )?; + + assert!(config1 < config2); + Ok(()) + } + + #[test] + fn test_ordering_by_machine_id_and_version() -> Result<()> { + let config1 = parse_bls_config( + r#" + title Entry 1 + version 3 + machine-id a + linux /vmlinuz-3 + initrd /initrd-3 + options opt1 + "#, + )?; + + let config2 = parse_bls_config( + r#" + title Entry 2 + version 5 + machine-id a + linux /vmlinuz-5 + initrd /initrd-5 + options opt2 + "#, + )?; + + assert!(config1 > config2); + Ok(()) + } } From 7018168403a99d54dd945598785c0d4894d50c47 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 14 Aug 2025 14:14:08 +0200 Subject: [PATCH 4/4] bls_config: use crate to compare UAPI versions Default string comparisons are wrong for this. Signed-off-by: Colin Walters --- Cargo.lock | 7 +++++ crates/lib/Cargo.toml | 1 + crates/lib/src/parsers/bls_config.rs | 41 ++++++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 32afbbc38..74584512b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,6 +279,7 @@ dependencies = [ "tokio-util", "toml 0.9.5", "tracing", + "uapi-version", "uuid", "xshell", ] @@ -2870,6 +2871,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "uapi-version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "849f6b1fe8a0fb07170737d7f3acf72cac5462fb3f4e86614474a49f7fac3b65" + [[package]] name = "unicase" version = "2.8.1" diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 7e6051244..76f8d0a57 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -64,6 +64,7 @@ serde_ignored = "0.1.10" serde_yaml = "0.9.34" tini = "1.3.0" uuid = { version = "1.8.0", features = ["v4"] } +uapi-version = "0.4.0" [dev-dependencies] similar-asserts = { workspace = true } diff --git a/crates/lib/src/parsers/bls_config.rs b/crates/lib/src/parsers/bls_config.rs index a6fcbd781..2f9ff34f6 100644 --- a/crates/lib/src/parsers/bls_config.rs +++ b/crates/lib/src/parsers/bls_config.rs @@ -5,6 +5,7 @@ use anyhow::{anyhow, Result}; use std::collections::HashMap; use std::fmt::Display; +use uapi_version::Version; /// Represents a single Boot Loader Specification config file. /// @@ -18,7 +19,9 @@ pub(crate) struct BLSConfig { pub(crate) title: Option, /// The version of the boot entry. /// See - pub(crate) version: String, + /// + /// This is hidden and must be accessed via [`Self::version()`]; + version: String, /// The path to the linux kernel to boot. pub(crate) linux: String, /// The paths to the initrd images. @@ -63,8 +66,7 @@ impl Ord for BLSConfig { } // Finally, sort by version in descending order. - // FIXME: This should use - self.version.cmp(&other.version).reverse() + self.version().cmp(&other.version()).reverse() } } @@ -97,6 +99,12 @@ impl Display for BLSConfig { } } +impl BLSConfig { + pub(crate) fn version(&self) -> Version { + Version::from(&self.version) + } +} + pub(crate) fn parse_bls_config(input: &str) -> Result { let mut title = None; let mut version = None; @@ -394,4 +402,31 @@ mod tests { assert!(config1 > config2); Ok(()) } + + #[test] + fn test_ordering_by_nontrivial_version() -> Result<()> { + let config_final = parse_bls_config( + r#" + title Entry 1 + version 1.0 + linux /vmlinuz-1 + initrd /initrd-1 + "#, + )?; + + let config_rc1 = parse_bls_config( + r#" + title Entry 2 + version 1.0~rc1 + linux /vmlinuz-2 + initrd /initrd-2 + "#, + )?; + + // In a sorted list, we want 1.0 to appear before 1.0~rc1 because + // versions are sorted descending. This means that in Rust's sort order, + // config_final should be "less than" config_rc1. + assert!(config_final < config_rc1); + Ok(()) + } }