mirror of
https://github.com/edera-dev/sprout.git
synced 2026-11-04 19:52:22 +00:00
feat(bls): one entry for each profile of a unified kernel image
A unified kernel image can have several profiles, each started by a .profile section and made of the sections after it on top of the base. Every profile is an entry. The first has the id of the file, and the others add @ and the identifier or the number of the profile, with a title that names the profile and the profile number as the load options. The profiles share the boot counter of the file, sort in the order they are in the image, and a profile after the first is not picked as the default entry when no default was asked for. The PE reader keeps every section in order, so repeated names and the sections that a profile removes can be told apart, and reads images with more than 96 sections.
This commit is contained in:
1 parent
ac49899294
commit
50f62f5bb6
7 files changed
+628
-86
No files matched your search
+313
-18
@@ -13,10 +13,13 @@ mod uki;
|
|||||||
|
|
||||||
pub use loader_conf::{LoaderConf, LoaderTimeout, RebootOnError};
|
pub use loader_conf::{LoaderConf, LoaderTimeout, RebootOnError};
|
||||||
pub use pe::{
|
pub use pe::{
|
||||||
MAX_SECTION_SIZE, PE_MACHINE_AARCH64, PE_MACHINE_X86_64, PeImage, ReadAt, read_pe,
|
MAX_SECTION_SIZE, PE_MACHINE_AARCH64, PE_MACHINE_X86_64, PeImage, PeSection, ReadAt, read_pe,
|
||||||
read_sections,
|
read_sections,
|
||||||
};
|
};
|
||||||
pub use uki::{OsRelease, UKI_SECTIONS};
|
pub use uki::{
|
||||||
|
MAX_PROFILES, OsRelease, ProfileInfo, UKI_SECTIONS, UkiProfile, profile_entry_id,
|
||||||
|
profile_id_suffix, profile_title, uki_profiles,
|
||||||
|
};
|
||||||
|
|
||||||
/// Represents a parsed BLS entry.
|
/// Represents a parsed BLS entry.
|
||||||
/// Fields unrelated to Sprout are not included.
|
/// Fields unrelated to Sprout are not included.
|
||||||
@@ -282,6 +285,12 @@ impl BlsEntry {
|
|||||||
self.machine_id.clone()
|
self.machine_id.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The number of the profile of a unified kernel image that the entry boots, which is zero
|
||||||
|
/// for the first profile and for entries that don't have one.
|
||||||
|
pub fn profile_number(&self) -> u32 {
|
||||||
|
self.profile.as_deref().and_then(parse_digits).unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether the entry has a boot counter with no tries left.
|
/// Whether the entry has a boot counter with no tries left.
|
||||||
pub fn is_bad(&self) -> bool {
|
pub fn is_bad(&self) -> bool {
|
||||||
self.boot_counter.is_some_and(|counter| counter.is_bad())
|
self.boot_counter.is_some_and(|counter| counter.is_bad())
|
||||||
@@ -347,21 +356,24 @@ impl BootCounter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decides which entries are the default. Each item of `entries` is `(is_default, is_bad)`,
|
/// Decides which entries are the default. Each item of `entries` is
|
||||||
/// and the result says whether each entry is the default afterwards.
|
/// `(is_default, is_bad, is_extra_profile)`, and the result says whether each entry is the
|
||||||
|
/// default afterwards.
|
||||||
/// Bad entries, which have no boot counter tries left, are never the default while a good entry
|
/// Bad entries, which have no boot counter tries left, are never the default while a good entry
|
||||||
/// exists. If no entry is a default, the first good entry is picked, or the first entry if every
|
/// exists. If no entry is a default, the first good entry is picked, which is not a profile of
|
||||||
/// entry is bad, so that there is always something to boot.
|
/// a unified kernel image after the first, unless there is nothing else. If every entry is bad,
|
||||||
pub fn resolve_default_flags(entries: &[(bool, bool)]) -> Vec<bool> {
|
/// the first entry is picked, so that there is always something to boot.
|
||||||
let any_good = entries.iter().any(|(_, bad)| !bad);
|
pub fn resolve_default_flags(entries: &[(bool, bool, bool)]) -> Vec<bool> {
|
||||||
|
let any_good = entries.iter().any(|(_, bad, _)| !bad);
|
||||||
let mut flags: Vec<bool> = entries
|
let mut flags: Vec<bool> = entries
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(default, bad)| *default && !(any_good && *bad))
|
.map(|(default, bad, _)| *default && !(any_good && *bad))
|
||||||
.collect();
|
.collect();
|
||||||
if !flags.iter().any(|default| *default) {
|
if !flags.iter().any(|default| *default) {
|
||||||
let pick = entries
|
let pick = entries
|
||||||
.iter()
|
.iter()
|
||||||
.position(|(_, bad)| !bad)
|
.position(|(_, bad, extra)| !bad && !extra)
|
||||||
|
.or_else(|| entries.iter().position(|(_, bad, _)| !bad))
|
||||||
.or_else(|| (!entries.is_empty()).then_some(0));
|
.or_else(|| (!entries.is_empty()).then_some(0));
|
||||||
if let Some(index) = pick {
|
if let Some(index) = pick {
|
||||||
flags[index] = true;
|
flags[index] = true;
|
||||||
@@ -429,7 +441,9 @@ pub fn sort_bls(a_bls: &BlsEntry, a_name: &str, b_bls: &BlsEntry, b_name: &str)
|
|||||||
.then_with(|| {
|
.then_with(|| {
|
||||||
compare_versions_optional(a_bls.version().as_deref(), b_bls.version().as_deref())
|
compare_versions_optional(a_bls.version().as_deref(), b_bls.version().as_deref())
|
||||||
.reverse()
|
.reverse()
|
||||||
}),
|
})
|
||||||
|
// The profiles of one image are in the order they are in the image.
|
||||||
|
.then_with(|| a_bls.profile_number().cmp(&b_bls.profile_number())),
|
||||||
(Some(_), None) => Ordering::Less,
|
(Some(_), None) => Ordering::Less,
|
||||||
(None, Some(_)) => Ordering::Greater,
|
(None, Some(_)) => Ordering::Greater,
|
||||||
(None, None) => Ordering::Equal,
|
(None, None) => Ordering::Equal,
|
||||||
@@ -443,6 +457,7 @@ pub fn sort_bls(a_bls: &BlsEntry, a_name: &str, b_bls: &BlsEntry, b_name: &str)
|
|||||||
// the boot counter: more tries left first, then fewer tries done.
|
// the boot counter: more tries left first, then fewer tries done.
|
||||||
ordering
|
ordering
|
||||||
.then_with(|| compare_versions(a_name, b_name).reverse())
|
.then_with(|| compare_versions(a_name, b_name).reverse())
|
||||||
|
.then_with(|| a_bls.profile_number().cmp(&b_bls.profile_number()))
|
||||||
.then_with(|| {
|
.then_with(|| {
|
||||||
let tries = |entry: &BlsEntry| {
|
let tries = |entry: &BlsEntry| {
|
||||||
entry
|
entry
|
||||||
@@ -786,7 +801,7 @@ mod tests {
|
|||||||
assert_eq!(entry.version.as_deref(), Some("40.1"));
|
assert_eq!(entry.version.as_deref(), Some("40.1"));
|
||||||
assert_eq!(entry.sort_key.as_deref(), Some("coreos"));
|
assert_eq!(entry.sort_key.as_deref(), Some("coreos"));
|
||||||
assert_eq!(entry.uname.as_deref(), Some("6.5.0"));
|
assert_eq!(entry.uname.as_deref(), Some("6.5.0"));
|
||||||
assert_eq!(entry.efi.as_deref(), Some("/EFI/Linux/fedora.efi"));
|
assert_eq!(entry.uki.as_deref(), Some("/EFI/Linux/fedora.efi"));
|
||||||
assert!(entry.options.is_none());
|
assert!(entry.options.is_none());
|
||||||
assert!(entry.is_valid());
|
assert!(entry.is_valid());
|
||||||
}
|
}
|
||||||
@@ -875,12 +890,284 @@ mod tests {
|
|||||||
assert_eq!(strip_extension(".efi", ".efi"), Some(("", ".efi")));
|
assert_eq!(strip_extension(".efi", ".efi"), Some(("", ".efi")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn text_section(name: &str, text: &str) -> PeSection {
|
||||||
|
PeSection {
|
||||||
|
name: name.to_string(),
|
||||||
|
data: Some(text.as_bytes().to_vec()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmdline(profile: &UkiProfile) -> Option<&[u8]> {
|
||||||
|
profile.sections.get(".cmdline").map(|data| data.as_slice())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn profiles_without_a_profile_section_are_a_single_base() {
|
||||||
|
let sections = [
|
||||||
|
text_section(".osrel", "ID=a"),
|
||||||
|
text_section(".cmdline", "q"),
|
||||||
|
];
|
||||||
|
let profiles = uki_profiles(§ions);
|
||||||
|
assert_eq!(profiles.len(), 1);
|
||||||
|
assert_eq!(profiles[0].index, None);
|
||||||
|
assert_eq!(cmdline(&profiles[0]), Some(&b"q"[..]));
|
||||||
|
assert!(profiles[0].sections.contains_key(".osrel"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn profiles_follow_the_spec_layout() {
|
||||||
|
let sections = [
|
||||||
|
text_section(".osrel", "ID=a"),
|
||||||
|
text_section(".cmdline", "base"),
|
||||||
|
text_section(".profile", "ID=p0"),
|
||||||
|
text_section(".profile", "ID=p1"),
|
||||||
|
text_section(".cmdline", "one"),
|
||||||
|
text_section(".profile", "ID=p2\nTITLE=Two"),
|
||||||
|
text_section(".cmdline", "two"),
|
||||||
|
];
|
||||||
|
let profiles = uki_profiles(§ions);
|
||||||
|
assert_eq!(profiles.len(), 3);
|
||||||
|
assert_eq!(
|
||||||
|
profiles.iter().map(|p| p.index).collect::<Vec<_>>(),
|
||||||
|
[Some(0), Some(1), Some(2)]
|
||||||
|
);
|
||||||
|
// The first profile has no sections of its own, so it uses the base.
|
||||||
|
assert_eq!(cmdline(&profiles[0]), Some(&b"base"[..]));
|
||||||
|
assert_eq!(cmdline(&profiles[1]), Some(&b"one"[..]));
|
||||||
|
assert_eq!(cmdline(&profiles[2]), Some(&b"two"[..]));
|
||||||
|
assert!(profiles.iter().all(|p| p.sections.contains_key(".osrel")));
|
||||||
|
assert_eq!(profiles[1].info.id.as_deref(), Some("p1"));
|
||||||
|
assert_eq!(profiles[2].info.title.as_deref(), Some("Two"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_first_section_of_a_name_wins_within_a_range() {
|
||||||
|
let sections = [
|
||||||
|
text_section(".profile", "ID=p"),
|
||||||
|
text_section(".cmdline", "a"),
|
||||||
|
text_section(".cmdline", "b"),
|
||||||
|
];
|
||||||
|
assert_eq!(cmdline(&uki_profiles(§ions)[0]), Some(&b"a"[..]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_empty_or_oversized_profile_section_masks_the_base() {
|
||||||
|
let sections = [
|
||||||
|
text_section(".cmdline", "base"),
|
||||||
|
text_section(".profile", "ID=empty"),
|
||||||
|
text_section(".cmdline", ""),
|
||||||
|
text_section(".profile", "ID=big"),
|
||||||
|
PeSection {
|
||||||
|
name: ".cmdline".to_string(),
|
||||||
|
data: None,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let profiles = uki_profiles(§ions);
|
||||||
|
assert_eq!(cmdline(&profiles[0]), None);
|
||||||
|
assert_eq!(cmdline(&profiles[1]), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_profile_can_override_the_os_release() {
|
||||||
|
let sections = [
|
||||||
|
text_section(".osrel", "ID=base\nPRETTY_NAME=Base"),
|
||||||
|
text_section(".profile", "ID=p"),
|
||||||
|
text_section(".osrel", "ID=other\nPRETTY_NAME=Other"),
|
||||||
|
];
|
||||||
|
let profiles = uki_profiles(§ions);
|
||||||
|
let entry = BlsEntry::from_uki_profile(&profiles[0], "/a.efi");
|
||||||
|
assert_eq!(entry.title.as_deref(), Some("Other"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_leading_profile_section_means_an_empty_base() {
|
||||||
|
let sections = [
|
||||||
|
text_section(".profile", "ID=p"),
|
||||||
|
text_section(".osrel", "ID=a"),
|
||||||
|
];
|
||||||
|
let profiles = uki_profiles(§ions);
|
||||||
|
assert_eq!(profiles.len(), 1);
|
||||||
|
assert!(profiles[0].sections.contains_key(".osrel"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn profiles_are_capped() {
|
||||||
|
let sections: Vec<PeSection> = (0..300)
|
||||||
|
.map(|index| text_section(".profile", &format!("ID=p{index}")))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(uki_profiles(§ions).len(), MAX_PROFILES);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn profile_info_parses_like_os_release() {
|
||||||
|
let info = ProfileInfo::parse("# c\nID=\"one\"\nTITLE='Hello World'\nID=two\n");
|
||||||
|
assert_eq!(info.id.as_deref(), Some("two"));
|
||||||
|
assert_eq!(info.title.as_deref(), Some("Hello World"));
|
||||||
|
let info = ProfileInfo::parse("ID=\nTITLE=\n");
|
||||||
|
assert_eq!(info, ProfileInfo::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn profile_ids_and_titles_follow_systemd_boot() {
|
||||||
|
let named = ProfileInfo {
|
||||||
|
id: Some("factory".to_string()),
|
||||||
|
title: None,
|
||||||
|
};
|
||||||
|
let titled = ProfileInfo {
|
||||||
|
id: Some("factory".to_string()),
|
||||||
|
title: Some("Factory Reset".to_string()),
|
||||||
|
};
|
||||||
|
let bare = ProfileInfo::default();
|
||||||
|
|
||||||
|
// The first profile has no suffix, the others have the id or their number.
|
||||||
|
assert_eq!(profile_id_suffix(0, &named), None);
|
||||||
|
assert_eq!(profile_id_suffix(1, &named).as_deref(), Some("factory"));
|
||||||
|
assert_eq!(profile_id_suffix(2, &bare).as_deref(), Some("2"));
|
||||||
|
|
||||||
|
assert_eq!(profile_title("Fedora", 0, &bare), "Fedora");
|
||||||
|
assert_eq!(
|
||||||
|
profile_title("Fedora", 0, &titled),
|
||||||
|
"Fedora (Factory Reset)"
|
||||||
|
);
|
||||||
|
assert_eq!(profile_title("Fedora", 1, &named), "Fedora (factory)");
|
||||||
|
assert_eq!(profile_title("Fedora", 1, &bare), "Fedora (Profile #2)");
|
||||||
|
assert_eq!(
|
||||||
|
profile_title("Fedora", 3, &titled),
|
||||||
|
"Fedora (Factory Reset)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn profile_entry_ids_only_keep_the_case_of_the_profile() {
|
||||||
|
assert_eq!(profile_entry_id("Fedora.EFI", None), "fedora.efi");
|
||||||
|
assert_eq!(
|
||||||
|
profile_entry_id("Fedora.EFI", Some("Factory")),
|
||||||
|
"fedora.efi@Factory"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn profile_entries_select_the_profile_with_load_options() {
|
||||||
|
let sections = [
|
||||||
|
text_section(".osrel", "ID=a"),
|
||||||
|
text_section(".profile", "ID=p0"),
|
||||||
|
text_section(".profile", "ID=p1"),
|
||||||
|
];
|
||||||
|
let profiles = uki_profiles(§ions);
|
||||||
|
let first = BlsEntry::from_uki_profile(&profiles[0], "/a.efi");
|
||||||
|
assert_eq!(first.chainload_options(), None);
|
||||||
|
let second = BlsEntry::from_uki_profile(&profiles[1], "/a.efi");
|
||||||
|
assert_eq!(second.chainload_options().as_deref(), Some("@1"));
|
||||||
|
assert_eq!(second.chainload_path().as_deref(), Some("a.efi"));
|
||||||
|
assert!(second.initrd_paths().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn profiles_of_one_image_sort_by_number() {
|
||||||
|
let sections = [
|
||||||
|
text_section(".osrel", "ID=a"),
|
||||||
|
text_section(".profile", "ID=p0"),
|
||||||
|
text_section(".profile", "ID=p1"),
|
||||||
|
];
|
||||||
|
let profiles = uki_profiles(§ions);
|
||||||
|
let first = BlsEntry::from_uki_profile(&profiles[0], "/a.efi");
|
||||||
|
let second = BlsEntry::from_uki_profile(&profiles[1], "/a.efi");
|
||||||
|
assert_eq!(sort_bls(&first, "a.efi", &second, "a.efi"), Ordering::Less);
|
||||||
|
assert_eq!(
|
||||||
|
sort_bls(&second, "a.efi", &first, "a.efi"),
|
||||||
|
Ordering::Greater
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_newer_version_sorts_before_the_profiles_of_an_older_one() {
|
||||||
|
let new = BlsEntry {
|
||||||
|
version: Some("2".to_string()),
|
||||||
|
profile: Some("1".to_string()),
|
||||||
|
..sort_entry(Some("linux"), None, Some("2"))
|
||||||
|
};
|
||||||
|
let old = BlsEntry {
|
||||||
|
version: Some("1".to_string()),
|
||||||
|
..sort_entry(Some("linux"), None, Some("1"))
|
||||||
|
};
|
||||||
|
assert_eq!(sort_bls(&new, "a", &old, "a"), Ordering::Less);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_flags_never_fall_back_to_extra_profiles() {
|
||||||
|
// Each item is (is_default, is_bad, is_extra_profile).
|
||||||
|
let flags = resolve_default_flags(&[(false, false, true), (false, false, false)]);
|
||||||
|
assert_eq!(flags, [false, true]);
|
||||||
|
// An extra profile can still be a default that was asked for.
|
||||||
|
let flags = resolve_default_flags(&[(true, false, true), (false, false, false)]);
|
||||||
|
assert_eq!(flags, [true, false]);
|
||||||
|
// With nothing else, an extra profile is better than nothing.
|
||||||
|
let flags = resolve_default_flags(&[(false, false, true)]);
|
||||||
|
assert_eq!(flags, [true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pe_keeps_duplicate_sections_in_order() {
|
||||||
|
let image = pe_image(&[
|
||||||
|
(".osrel", b"ID=a"),
|
||||||
|
(".profile", b"ID=one"),
|
||||||
|
(".cmdline", b"x"),
|
||||||
|
(".profile", b"ID=two"),
|
||||||
|
(".cmdline", b"y"),
|
||||||
|
(".text", b"code"),
|
||||||
|
]);
|
||||||
|
let pe = read_pe(&mut image.as_slice(), &[".osrel", ".profile", ".cmdline"]).unwrap();
|
||||||
|
let names: Vec<_> = pe
|
||||||
|
.sections
|
||||||
|
.iter()
|
||||||
|
.map(|section| section.name.as_str())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
names,
|
||||||
|
[".osrel", ".profile", ".cmdline", ".profile", ".cmdline"]
|
||||||
|
);
|
||||||
|
assert_eq!(pe.sections[3].data.as_deref(), Some(&b"ID=two"[..]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pe_keeps_oversized_sections_without_data() {
|
||||||
|
let big = vec![b'a'; MAX_SECTION_SIZE as usize + 1];
|
||||||
|
let image = pe_image(&[(".cmdline", &big), (".osrel", b"ID=x")]);
|
||||||
|
let pe = read_pe(&mut image.as_slice(), &[".cmdline", ".osrel"]).unwrap();
|
||||||
|
assert_eq!(pe.sections[0].name, ".cmdline");
|
||||||
|
assert_eq!(pe.sections[0].data, None);
|
||||||
|
assert_eq!(pe.sections[1].data.as_deref(), Some(&b"ID=x"[..]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pe_accepts_many_sections_and_rejects_too_many() {
|
||||||
|
let many: Vec<(&str, &[u8])> = (0..300).map(|_| (".dtbauto", &b"d"[..])).collect();
|
||||||
|
let image = pe_image(&many);
|
||||||
|
let pe = read_pe(&mut image.as_slice(), &[".dtbauto"]).unwrap();
|
||||||
|
assert_eq!(pe.sections.len(), 300);
|
||||||
|
|
||||||
|
let mut image = pe_image(&[(".osrel", b"ID=x")]);
|
||||||
|
image[0x86..0x88].copy_from_slice(&5000u16.to_le_bytes());
|
||||||
|
assert!(read_pe(&mut image.as_slice(), &[".osrel"]).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pe_zero_virtual_size_means_an_empty_section() {
|
||||||
|
let mut image = pe_image(&[(".cmdline", b"quiet")]);
|
||||||
|
// The virtual size is at offset 8 of the first section table entry.
|
||||||
|
let entry = 0x80 + 24;
|
||||||
|
image[entry + 8..entry + 12].copy_from_slice(&0u32.to_le_bytes());
|
||||||
|
let pe = read_pe(&mut image.as_slice(), &[".cmdline"]).unwrap();
|
||||||
|
assert_eq!(pe.sections[0].data.as_deref(), Some(&b""[..]));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pe_reports_the_machine_type() {
|
fn pe_reports_the_machine_type() {
|
||||||
let image = pe_image(&[(".osrel", b"ID=x")]);
|
let image = pe_image(&[(".osrel", b"ID=x")]);
|
||||||
let pe = read_pe(&mut image.as_slice(), &[".osrel"]).unwrap();
|
let pe = read_pe(&mut image.as_slice(), &[".osrel"]).unwrap();
|
||||||
assert_eq!(pe.machine, PE_MACHINE_X86_64);
|
assert_eq!(pe.machine, PE_MACHINE_X86_64);
|
||||||
assert!(pe.sections.contains_key(".osrel"));
|
assert_eq!(pe.sections.len(), 1);
|
||||||
|
assert_eq!(pe.sections[0].name, ".osrel");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1122,19 +1409,27 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn default_flags_keep_a_good_default() {
|
fn default_flags_keep_a_good_default() {
|
||||||
// The second entry is bad and also matched the default pattern.
|
// The second entry is bad and also matched the default pattern.
|
||||||
let flags = resolve_default_flags(&[(true, false), (true, true), (false, false)]);
|
let flags = resolve_default_flags(&[
|
||||||
|
(true, false, false),
|
||||||
|
(true, true, false),
|
||||||
|
(false, false, false),
|
||||||
|
]);
|
||||||
assert_eq!(flags, [true, false, false]);
|
assert_eq!(flags, [true, false, false]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_flags_replace_a_bad_default_with_the_first_good_entry() {
|
fn default_flags_replace_a_bad_default_with_the_first_good_entry() {
|
||||||
let flags = resolve_default_flags(&[(false, false), (true, true), (false, false)]);
|
let flags = resolve_default_flags(&[
|
||||||
|
(false, false, false),
|
||||||
|
(true, true, false),
|
||||||
|
(false, false, false),
|
||||||
|
]);
|
||||||
assert_eq!(flags, [true, false, false]);
|
assert_eq!(flags, [true, false, false]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_flags_fallback_skips_bad_entries() {
|
fn default_flags_fallback_skips_bad_entries() {
|
||||||
let flags = resolve_default_flags(&[(false, true), (false, false)]);
|
let flags = resolve_default_flags(&[(false, true, false), (false, false, false)]);
|
||||||
assert_eq!(flags, [false, true]);
|
assert_eq!(flags, [false, true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1142,11 +1437,11 @@ mod tests {
|
|||||||
fn default_flags_when_every_entry_is_bad() {
|
fn default_flags_when_every_entry_is_bad() {
|
||||||
// A configured default stays, otherwise the first entry is the default.
|
// A configured default stays, otherwise the first entry is the default.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_default_flags(&[(false, true), (true, true)]),
|
resolve_default_flags(&[(false, true, false), (true, true, false)]),
|
||||||
[false, true]
|
[false, true]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_default_flags(&[(false, true), (false, true)]),
|
resolve_default_flags(&[(false, true, false), (false, true, false)]),
|
||||||
[true, false]
|
[true, false]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-16
@@ -14,8 +14,9 @@ pub const PE_MACHINE_X86_64: u16 = 0x8664;
|
|||||||
/// The machine type of an aarch64 PE image.
|
/// The machine type of an aarch64 PE image.
|
||||||
pub const PE_MACHINE_AARCH64: u16 = 0xaa64;
|
pub const PE_MACHINE_AARCH64: u16 = 0xaa64;
|
||||||
|
|
||||||
/// The most sections a PE image can have.
|
/// The most sections that are read from a PE image. A unified kernel image with many profiles
|
||||||
const MAX_SECTIONS: usize = 96;
|
/// has many more sections than the 96 that Windows allows, and the table is still small.
|
||||||
|
const MAX_SECTIONS: usize = 1024;
|
||||||
|
|
||||||
/// The size of a section table entry.
|
/// The size of a section table entry.
|
||||||
const SECTION_ENTRY_SIZE: usize = 40;
|
const SECTION_ENTRY_SIZE: usize = 40;
|
||||||
@@ -59,22 +60,40 @@ fn u32_at(bytes: &[u8], offset: usize) -> u32 {
|
|||||||
pub struct PeImage {
|
pub struct PeImage {
|
||||||
/// The machine type from the COFF header, such as [PE_MACHINE_X86_64].
|
/// The machine type from the COFF header, such as [PE_MACHINE_X86_64].
|
||||||
pub machine: u16,
|
pub machine: u16,
|
||||||
/// The contents of the wanted sections that were found.
|
/// The wanted sections, in the order of the section table, including repeated names.
|
||||||
pub sections: BTreeMap<String, Vec<u8>>,
|
pub sections: Vec<PeSection>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the sections named in `wanted` from the PE image in `reader`.
|
/// A wanted section of a PE image.
|
||||||
/// See [read_pe] for the details.
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct PeSection {
|
||||||
|
/// The name of the section, such as `.osrel`.
|
||||||
|
pub name: String,
|
||||||
|
/// The contents of the section, or None if it is larger than [MAX_SECTION_SIZE]. A section
|
||||||
|
/// without contents is still there, as it takes the place of a section with the same name.
|
||||||
|
pub data: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the sections named in `wanted` from the PE image in `reader`, keeping the first
|
||||||
|
/// section of each name. See [read_pe] for the details.
|
||||||
pub fn read_sections<R: ReadAt>(
|
pub fn read_sections<R: ReadAt>(
|
||||||
reader: &mut R,
|
reader: &mut R,
|
||||||
wanted: &[&str],
|
wanted: &[&str],
|
||||||
) -> Result<BTreeMap<String, Vec<u8>>> {
|
) -> Result<BTreeMap<String, Vec<u8>>> {
|
||||||
Ok(read_pe(reader, wanted)?.sections)
|
let mut sections = BTreeMap::new();
|
||||||
|
for section in read_pe(reader, wanted)?.sections {
|
||||||
|
sections.entry(section.name).or_insert(section.data);
|
||||||
|
}
|
||||||
|
// A section that was too large is not available.
|
||||||
|
Ok(sections
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(name, data)| data.map(|data| (name, data)))
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the machine type and the sections named in `wanted` from the PE image in `reader`.
|
/// Reads the machine type and the sections named in `wanted` from the PE image in `reader`.
|
||||||
/// Only the headers and the wanted sections are read, so this is cheap on large images.
|
/// Only the headers and the wanted sections are read, so this is cheap on large images.
|
||||||
/// A wanted section that is missing, or larger than [MAX_SECTION_SIZE], is left out.
|
/// A wanted section that is larger than [MAX_SECTION_SIZE] is listed without its contents.
|
||||||
/// An image that isn't a valid PE file is an error.
|
/// An image that isn't a valid PE file is an error.
|
||||||
pub fn read_pe<R: ReadAt>(reader: &mut R, wanted: &[&str]) -> Result<PeImage> {
|
pub fn read_pe<R: ReadAt>(reader: &mut R, wanted: &[&str]) -> Result<PeImage> {
|
||||||
// The DOS header starts with "MZ" and holds the offset of the PE header at 0x3c.
|
// The DOS header starts with "MZ" and holds the offset of the PE header at 0x3c.
|
||||||
@@ -102,14 +121,14 @@ pub fn read_pe<R: ReadAt>(reader: &mut R, wanted: &[&str]) -> Result<PeImage> {
|
|||||||
let mut table = vec![0u8; section_count * SECTION_ENTRY_SIZE];
|
let mut table = vec![0u8; section_count * SECTION_ENTRY_SIZE];
|
||||||
reader.read_at(pe_offset + 24 + optional_header_size, &mut table)?;
|
reader.read_at(pe_offset + 24 + optional_header_size, &mut table)?;
|
||||||
|
|
||||||
let mut sections = BTreeMap::new();
|
let mut sections = Vec::new();
|
||||||
for entry in table.as_chunks::<SECTION_ENTRY_SIZE>().0 {
|
for entry in table.as_chunks::<SECTION_ENTRY_SIZE>().0 {
|
||||||
// The name is up to eight bytes, padded with NUL.
|
// The name is up to eight bytes, padded with NUL.
|
||||||
let name_len = entry[..8].iter().position(|b| *b == 0).unwrap_or(8);
|
let name_len = entry[..8].iter().position(|b| *b == 0).unwrap_or(8);
|
||||||
let Ok(name) = core::str::from_utf8(&entry[..name_len]) else {
|
let Ok(name) = core::str::from_utf8(&entry[..name_len]) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if !wanted.contains(&name) || sections.contains_key(name) {
|
if !wanted.contains(&name) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,18 +136,23 @@ pub fn read_pe<R: ReadAt>(reader: &mut R, wanted: &[&str]) -> Result<PeImage> {
|
|||||||
let virtual_size = u64::from(u32_at(entry, 8));
|
let virtual_size = u64::from(u32_at(entry, 8));
|
||||||
let raw_size = u64::from(u32_at(entry, 16));
|
let raw_size = u64::from(u32_at(entry, 16));
|
||||||
let raw_offset = u64::from(u32_at(entry, 20));
|
let raw_offset = u64::from(u32_at(entry, 20));
|
||||||
let size = if virtual_size == 0 {
|
// A virtual size of zero makes the section empty, which is how a profile removes a section
|
||||||
raw_size
|
// of the base image.
|
||||||
} else {
|
let size = virtual_size.min(raw_size);
|
||||||
virtual_size.min(raw_size)
|
|
||||||
};
|
|
||||||
if size > MAX_SECTION_SIZE {
|
if size > MAX_SECTION_SIZE {
|
||||||
|
sections.push(PeSection {
|
||||||
|
name: name.to_string(),
|
||||||
|
data: None,
|
||||||
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut data = vec![0u8; size as usize];
|
let mut data = vec![0u8; size as usize];
|
||||||
reader.read_at(raw_offset, &mut data)?;
|
reader.read_at(raw_offset, &mut data)?;
|
||||||
sections.insert(name.to_string(), data);
|
sections.push(PeSection {
|
||||||
|
name: name.to_string(),
|
||||||
|
data: Some(data),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(PeImage { machine, sections })
|
Ok(PeImage { machine, sections })
|
||||||
|
|||||||
+151
-5
@@ -1,10 +1,14 @@
|
|||||||
use crate::BlsEntry;
|
use crate::{BlsEntry, PeSection};
|
||||||
use alloc::collections::BTreeMap;
|
use alloc::collections::BTreeMap;
|
||||||
|
use alloc::format;
|
||||||
use alloc::string::{String, ToString};
|
use alloc::string::{String, ToString};
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
/// The PE sections of a unified kernel image that Sprout reads.
|
/// The PE sections of a unified kernel image that Sprout reads.
|
||||||
pub const UKI_SECTIONS: [&str; 3] = [".osrel", ".cmdline", ".uname"];
|
pub const UKI_SECTIONS: [&str; 4] = [".osrel", ".cmdline", ".uname", ".profile"];
|
||||||
|
|
||||||
|
/// The most profiles that are read from a unified kernel image.
|
||||||
|
pub const MAX_PROFILES: usize = 256;
|
||||||
|
|
||||||
/// A parsed os-release file, such as the `.osrel` section of a unified kernel image.
|
/// A parsed os-release file, such as the `.osrel` section of a unified kernel image.
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
@@ -68,12 +72,26 @@ fn section_text(sections: &BTreeMap<String, Vec<u8>>, name: &str) -> Option<Stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BlsEntry {
|
impl BlsEntry {
|
||||||
/// Produces an entry for the unified kernel image at `efi_path`, from its PE `sections`.
|
/// Produces an entry for one profile of the unified kernel image at `uki_path`.
|
||||||
|
/// A profile after the first is booted with `@<number>` in its load options.
|
||||||
|
pub fn from_uki_profile(profile: &UkiProfile, uki_path: &str) -> Self {
|
||||||
|
let mut entry = Self::from_uki(&profile.sections, uki_path);
|
||||||
|
if let Some(index) = profile.index {
|
||||||
|
entry.profile = (index > 0).then(|| index.to_string());
|
||||||
|
entry.title = entry
|
||||||
|
.title
|
||||||
|
.take()
|
||||||
|
.map(|title| profile_title(&title, index, &profile.info));
|
||||||
|
}
|
||||||
|
entry
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produces an entry for the unified kernel image at `uki_path`, from its PE `sections`.
|
||||||
/// The title comes from `PRETTY_NAME`, then `ID`. The version comes from `IMAGE_VERSION`,
|
/// The title comes from `PRETTY_NAME`, then `ID`. The version comes from `IMAGE_VERSION`,
|
||||||
/// `VERSION_ID`, `BUILD_ID`, then the `.uname` section. The sort key comes from `IMAGE_ID`,
|
/// `VERSION_ID`, `BUILD_ID`, then the `.uname` section. The sort key comes from `IMAGE_ID`,
|
||||||
/// then `ID`. The embedded command line is kept in `cmdline` and not in `options`, as the
|
/// then `ID`. The embedded command line is kept in `cmdline` and not in `options`, as the
|
||||||
/// image reads its own command line.
|
/// image reads its own command line.
|
||||||
pub fn from_uki(sections: &BTreeMap<String, Vec<u8>>, efi_path: &str) -> Self {
|
pub fn from_uki(sections: &BTreeMap<String, Vec<u8>>, uki_path: &str) -> Self {
|
||||||
let os_release = section_text(sections, ".osrel")
|
let os_release = section_text(sections, ".osrel")
|
||||||
.map(|text| OsRelease::parse(&text))
|
.map(|text| OsRelease::parse(&text))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
@@ -89,10 +107,138 @@ impl BlsEntry {
|
|||||||
title: first(&["PRETTY_NAME", "ID"]),
|
title: first(&["PRETTY_NAME", "ID"]),
|
||||||
version: first(&["IMAGE_VERSION", "VERSION_ID", "BUILD_ID"]).or_else(|| uname.clone()),
|
version: first(&["IMAGE_VERSION", "VERSION_ID", "BUILD_ID"]).or_else(|| uname.clone()),
|
||||||
sort_key: first(&["IMAGE_ID", "ID"]),
|
sort_key: first(&["IMAGE_ID", "ID"]),
|
||||||
efi: Some(efi_path.to_string()),
|
uki: Some(uki_path.to_string()),
|
||||||
cmdline: section_text(sections, ".cmdline"),
|
cmdline: section_text(sections, ".cmdline"),
|
||||||
uname,
|
uname,
|
||||||
..Self::default()
|
..Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The metadata in the `.profile` section of a profile of a unified kernel image.
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ProfileInfo {
|
||||||
|
/// A short identifier of the profile.
|
||||||
|
pub id: Option<String>,
|
||||||
|
/// A title for people to read.
|
||||||
|
pub title: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProfileInfo {
|
||||||
|
/// Parses the text of a `.profile` section, which is in the format of an os-release file.
|
||||||
|
/// A key without a value is treated as missing.
|
||||||
|
pub fn parse(text: &str) -> Self {
|
||||||
|
let os_release = OsRelease::parse(text);
|
||||||
|
let get = |key: &str| {
|
||||||
|
os_release
|
||||||
|
.get(key)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(ToString::to_string)
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
id: get("ID"),
|
||||||
|
title: get("TITLE"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sections that one profile of a unified kernel image boots with.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct UkiProfile {
|
||||||
|
/// The number of the profile, or None if the image has no `.profile` sections.
|
||||||
|
pub index: Option<u32>,
|
||||||
|
/// The metadata of the profile.
|
||||||
|
pub info: ProfileInfo,
|
||||||
|
/// The sections of the profile on top of those of the base image. A section that is empty
|
||||||
|
/// or too large to read, which is how a profile removes a section, is not here.
|
||||||
|
pub sections: BTreeMap<String, Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keeps the first section of each name in `range`.
|
||||||
|
fn first_of_each(range: &[PeSection]) -> BTreeMap<String, Option<Vec<u8>>> {
|
||||||
|
let mut sections = BTreeMap::new();
|
||||||
|
for section in range {
|
||||||
|
sections
|
||||||
|
.entry(section.name.clone())
|
||||||
|
.or_insert_with(|| section.data.clone());
|
||||||
|
}
|
||||||
|
sections
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Splits the `sections` of a unified kernel image, in the order of the section table, into the
|
||||||
|
/// profiles of the image. Each `.profile` section starts a profile that goes on until the next
|
||||||
|
/// one, and the sections before the first are the base that every profile starts from.
|
||||||
|
/// A section of a profile takes the place of the section of the same name in the base.
|
||||||
|
/// An image without `.profile` sections has one profile, which is the base.
|
||||||
|
pub fn uki_profiles(sections: &[PeSection]) -> Vec<UkiProfile> {
|
||||||
|
// Sections that are empty or too large are not available.
|
||||||
|
let usable = |map: BTreeMap<String, Option<Vec<u8>>>| -> BTreeMap<String, Vec<u8>> {
|
||||||
|
map.into_iter()
|
||||||
|
.filter_map(|(name, data)| {
|
||||||
|
data.filter(|data| !data.is_empty())
|
||||||
|
.map(|data| (name, data))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let starts: Vec<usize> = sections
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, section)| section.name == ".profile")
|
||||||
|
.map(|(index, _)| index)
|
||||||
|
.collect();
|
||||||
|
let Some(&first) = starts.first() else {
|
||||||
|
return alloc::vec![UkiProfile {
|
||||||
|
index: None,
|
||||||
|
info: ProfileInfo::default(),
|
||||||
|
sections: usable(first_of_each(sections)),
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
|
||||||
|
let base = first_of_each(§ions[..first]);
|
||||||
|
starts
|
||||||
|
.iter()
|
||||||
|
.take(MAX_PROFILES)
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, start)| {
|
||||||
|
let end = starts.get(index + 1).copied().unwrap_or(sections.len());
|
||||||
|
let range = §ions[*start..end];
|
||||||
|
let mut merged = base.clone();
|
||||||
|
merged.extend(first_of_each(range));
|
||||||
|
UkiProfile {
|
||||||
|
index: Some(index as u32),
|
||||||
|
info: range[0]
|
||||||
|
.data
|
||||||
|
.as_ref()
|
||||||
|
.map(|data| ProfileInfo::parse(&String::from_utf8_lossy(data)))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
sections: usable(merged),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The part of the id of an entry that comes after the `@`, which names the profile.
|
||||||
|
/// The first profile has none, and the others have their identifier or their number.
|
||||||
|
pub fn profile_id_suffix(index: u32, info: &ProfileInfo) -> Option<String> {
|
||||||
|
(index > 0).then(|| info.id.clone().unwrap_or_else(|| index.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The title of the entry for a profile, given the `name` of the image.
|
||||||
|
pub fn profile_title(name: &str, index: u32, info: &ProfileInfo) -> String {
|
||||||
|
match (&info.title, &info.id) {
|
||||||
|
(Some(title), _) => format!("{} ({})", name, title),
|
||||||
|
(None, Some(id)) if index > 0 => format!("{} ({})", name, id),
|
||||||
|
(None, None) if index > 0 => format!("{} (Profile #{})", name, index + 1),
|
||||||
|
_ => name.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The id of the entry for a profile of the image with the file name `file_id`, such as
|
||||||
|
/// `fedora.efi`. The file name is in lower case, as in systemd-boot, and the profile is not.
|
||||||
|
pub fn profile_entry_id(file_id: &str, suffix: Option<&str>) -> String {
|
||||||
|
match suffix {
|
||||||
|
Some(suffix) => format!("{}@{}", file_id.to_lowercase(), suffix),
|
||||||
|
None => file_id.to_lowercase(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ use crate::context::SproutContext;
|
|||||||
use alloc::format;
|
use alloc::format;
|
||||||
use alloc::rc::Rc;
|
use alloc::rc::Rc;
|
||||||
use alloc::string::{String, ToString};
|
use alloc::string::{String, ToString};
|
||||||
|
use edera_sprout_bls::profile_entry_id;
|
||||||
use edera_sprout_config::entries::EntryDeclaration;
|
use edera_sprout_config::entries::EntryDeclaration;
|
||||||
use edera_sprout_parsing::{fnmatch_ignore_case, glob_match};
|
use edera_sprout_parsing::{fnmatch_ignore_case, glob_match};
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ pub struct BootableEntry {
|
|||||||
sort_key: Option<String>,
|
sort_key: Option<String>,
|
||||||
boot_counter: Option<BootCounterTarget>,
|
boot_counter: Option<BootCounterTarget>,
|
||||||
id_suffix: Option<String>,
|
id_suffix: Option<String>,
|
||||||
|
id_profile: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BootableEntry {
|
impl BootableEntry {
|
||||||
@@ -38,6 +40,7 @@ impl BootableEntry {
|
|||||||
sort_key: None,
|
sort_key: None,
|
||||||
boot_counter: None,
|
boot_counter: None,
|
||||||
id_suffix: None,
|
id_suffix: None,
|
||||||
|
id_profile: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,11 +54,38 @@ impl BootableEntry {
|
|||||||
/// `fedora.conf`, in lower case. Any other entry has its name as the id.
|
/// `fedora.conf`, in lower case. Any other entry has its name as the id.
|
||||||
pub fn id(&self) -> String {
|
pub fn id(&self) -> String {
|
||||||
match self.id_suffix {
|
match self.id_suffix {
|
||||||
Some(ref suffix) => format!("{}{}", self.name, suffix).to_lowercase(),
|
Some(ref suffix) => {
|
||||||
|
let file = format!("{}{}", self.sort_name(), suffix);
|
||||||
|
profile_entry_id(&file, self.id_profile.as_deref())
|
||||||
|
}
|
||||||
None => self.name.clone(),
|
None => self.name.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch the name of the entry without the profile of a unified kernel image, which is
|
||||||
|
/// the same for every profile of the same image.
|
||||||
|
pub fn sort_name(&self) -> &str {
|
||||||
|
self.id_profile
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|profile| {
|
||||||
|
self.name
|
||||||
|
.strip_suffix(profile)
|
||||||
|
.and_then(|name| name.strip_suffix('@'))
|
||||||
|
})
|
||||||
|
.unwrap_or(&self.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch whether the entry is a profile of a unified kernel image after the first, which
|
||||||
|
/// is never picked as the default entry unless it was asked for.
|
||||||
|
pub fn is_extra_profile(&self) -> bool {
|
||||||
|
self.id_profile.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the profile of a unified kernel image that is part of the id of this entry.
|
||||||
|
pub fn set_id_profile(&mut self, profile: &str) {
|
||||||
|
self.id_profile = Some(profile.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the file extension that is part of the id of this entry, such as `.conf`.
|
/// Set the file extension that is part of the id of this entry, such as `.conf`.
|
||||||
pub fn set_id_suffix(&mut self, suffix: &str) {
|
pub fn set_id_suffix(&mut self, suffix: &str) {
|
||||||
self.id_suffix = Some(suffix.to_string());
|
self.id_suffix = Some(suffix.to_string());
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ use alloc::{
|
|||||||
};
|
};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use core::{cmp::Ordering, str::FromStr};
|
use core::{cmp::Ordering, str::FromStr};
|
||||||
use edera_sprout_bls::{BlsEntry, BootCounter, is_reserved_entry_name, sort_bls, strip_extension};
|
use edera_sprout_bls::{
|
||||||
|
BlsEntry, BootCounter, is_reserved_entry_name, profile_id_suffix, sort_bls, strip_extension,
|
||||||
|
};
|
||||||
use edera_sprout_config::generators::bls::BlsConfiguration;
|
use edera_sprout_config::generators::bls::BlsConfiguration;
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use uefi::{
|
use uefi::{
|
||||||
@@ -54,7 +56,7 @@ fn quirk_initrd_remove_tuned(paths: Vec<String>) -> Vec<String> {
|
|||||||
fn sort_entries(a: &(BlsEntry, BootableEntry), b: &(BlsEntry, BootableEntry)) -> Ordering {
|
fn sort_entries(a: &(BlsEntry, BootableEntry), b: &(BlsEntry, BootableEntry)) -> Ordering {
|
||||||
let (a_bls, a_boot) = a;
|
let (a_bls, a_boot) = a;
|
||||||
let (b_bls, b_boot) = b;
|
let (b_bls, b_boot) = b;
|
||||||
sort_bls(a_bls, a_boot.name(), b_bls, b_boot.name())
|
sort_bls(a_bls, a_boot.sort_name(), b_bls, b_boot.sort_name())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Produces the bootable entry for the BLS `entry` with the id `name` and the `initrds`.
|
/// Produces the bootable entry for the BLS `entry` with the id `name` and the `initrds`.
|
||||||
@@ -297,8 +299,7 @@ fn generate_type2(
|
|||||||
|
|
||||||
let mut found: Vec<(BlsEntry, BootableEntry)> = Vec::new();
|
let mut found: Vec<(BlsEntry, BootableEntry)> = Vec::new();
|
||||||
for image in uki::scan(resolved.filesystem_handle, &directory_name)? {
|
for image in uki::scan(resolved.filesystem_handle, &directory_name)? {
|
||||||
// Only unified kernel images are entries. Images built for another architecture, and
|
// Only unified kernel images that are built for this machine are entries.
|
||||||
// other EFI programs that have no os-release section, are skipped.
|
|
||||||
if image.machine != PE_MACHINE {
|
if image.machine != PE_MACHINE {
|
||||||
info!(
|
info!(
|
||||||
"skipping {} as it is built for another architecture",
|
"skipping {} as it is built for another architecture",
|
||||||
@@ -306,13 +307,6 @@ fn generate_type2(
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !image.has_osrel {
|
|
||||||
info!(
|
|
||||||
"skipping {} as it has no .osrel section, so it is not a unified kernel image",
|
|
||||||
image.file_name
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The id is the file name without the extension and the boot counter.
|
// The id is the file name without the extension and the boot counter.
|
||||||
let file_name = image.file_name;
|
let file_name = image.file_name;
|
||||||
@@ -329,37 +323,71 @@ fn generate_type2(
|
|||||||
}
|
}
|
||||||
let id = id.to_string();
|
let id = id.to_string();
|
||||||
|
|
||||||
let mut entry = image.entry;
|
// Every profile of the image is an entry. They share the file, and so the boot counter.
|
||||||
entry.boot_counter = boot_counter;
|
let mut suffixes: Vec<String> = Vec::new();
|
||||||
|
for profile in image.profiles {
|
||||||
|
// A profile without an os-release section is not a unified kernel image, such as
|
||||||
|
// other EFI programs.
|
||||||
|
if !profile.has_osrel {
|
||||||
|
info!(
|
||||||
|
"skipping {} as it has no .osrel section, so it is not a unified kernel image",
|
||||||
|
file_name
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let mut boot = bootable_entry(context, bls, &id, &entry, &[]);
|
// The first profile has the id of the file, and the others name their profile.
|
||||||
boot.set_id_suffix(&extension);
|
// A profile that repeats the identifier of an earlier one is told apart by its number.
|
||||||
|
let index = profile.index.unwrap_or(0);
|
||||||
|
let mut suffix = profile_id_suffix(index, &profile.info);
|
||||||
|
if let Some(ref name) = suffix
|
||||||
|
&& suffixes.contains(name)
|
||||||
|
{
|
||||||
|
suffix = Some(index.to_string());
|
||||||
|
}
|
||||||
|
if let Some(ref name) = suffix {
|
||||||
|
suffixes.push(name.clone());
|
||||||
|
}
|
||||||
|
let name = match suffix {
|
||||||
|
Some(ref suffix) => format!("{}@{}", id, suffix),
|
||||||
|
None => id.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
// An image with the same id as another entry, such as a leftover copy with another boot
|
let mut entry = profile.entry;
|
||||||
// counter, would be ambiguous. Type 1 and type 2 entries have different ids.
|
entry.boot_counter = boot_counter;
|
||||||
if entries
|
|
||||||
.iter()
|
let mut boot = bootable_entry(context, bls, &name, &entry, &[]);
|
||||||
.chain(found.iter())
|
boot.set_id_suffix(&extension);
|
||||||
.any(|(_, other)| other.id() == boot.id())
|
if let Some(ref suffix) = suffix {
|
||||||
{
|
boot.set_id_profile(suffix);
|
||||||
warn!(
|
}
|
||||||
"unified kernel image {} has the same id as another entry, skipping",
|
|
||||||
file_name
|
// An image with the same id as another entry, such as a leftover copy with another
|
||||||
);
|
// boot counter, would be ambiguous. Type 1 and type 2 entries have different ids.
|
||||||
continue;
|
if entries
|
||||||
|
.iter()
|
||||||
|
.chain(found.iter())
|
||||||
|
.any(|(_, other)| other.id() == boot.id())
|
||||||
|
{
|
||||||
|
warn!(
|
||||||
|
"unified kernel image {} has the same id as another entry, skipping",
|
||||||
|
file_name
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(counter) = boot_counter {
|
||||||
|
boot.set_boot_counter(BootCounterTarget {
|
||||||
|
counter,
|
||||||
|
filesystem: resolved.filesystem_handle,
|
||||||
|
directory: PathBuf::from(directory.clone()),
|
||||||
|
id: id.clone(),
|
||||||
|
file_name: file_name.clone(),
|
||||||
|
extension: extension.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
found.push((entry, boot));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(counter) = boot_counter {
|
|
||||||
boot.set_boot_counter(BootCounterTarget {
|
|
||||||
counter,
|
|
||||||
filesystem: resolved.filesystem_handle,
|
|
||||||
directory: PathBuf::from(directory.clone()),
|
|
||||||
id,
|
|
||||||
file_name,
|
|
||||||
extension,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
found.push((entry, boot));
|
|
||||||
}
|
}
|
||||||
Ok(found)
|
Ok(found)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ use alloc::{
|
|||||||
vec::Vec,
|
vec::Vec,
|
||||||
};
|
};
|
||||||
use anyhow::{Context, Result, anyhow, bail};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
use edera_sprout_bls::{BlsEntry, PeImage, ReadAt, UKI_SECTIONS, read_pe, strip_extension};
|
use edera_sprout_bls::{
|
||||||
|
BlsEntry, PeImage, ProfileInfo, ReadAt, UKI_SECTIONS, read_pe, strip_extension, uki_profiles,
|
||||||
|
};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use uefi::{
|
use uefi::{
|
||||||
CString16, Handle, Status,
|
CString16, Handle, Status,
|
||||||
@@ -18,9 +20,19 @@ pub struct UkiFile {
|
|||||||
pub file_name: String,
|
pub file_name: String,
|
||||||
/// The machine type of the image.
|
/// The machine type of the image.
|
||||||
pub machine: u16,
|
pub machine: u16,
|
||||||
/// Whether the image has an `.osrel` section, which unified kernel images always have.
|
/// The profiles of the image. An image without profiles has one.
|
||||||
|
pub profiles: Vec<UkiProfileEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One profile of a unified kernel image.
|
||||||
|
pub struct UkiProfileEntry {
|
||||||
|
/// The number of the profile, or None if the image has no profiles.
|
||||||
|
pub index: Option<u32>,
|
||||||
|
/// The metadata of the profile.
|
||||||
|
pub info: ProfileInfo,
|
||||||
|
/// Whether the profile has an `.osrel` section, which unified kernel images always have.
|
||||||
pub has_osrel: bool,
|
pub has_osrel: bool,
|
||||||
/// The entry made from the sections of the image.
|
/// The entry made from the sections of the profile.
|
||||||
pub entry: BlsEntry,
|
pub entry: BlsEntry,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,9 +106,16 @@ pub fn scan(filesystem: Handle, directory: &str) -> Result<Vec<UkiFile>> {
|
|||||||
Ok(image) => found.push(UkiFile {
|
Ok(image) => found.push(UkiFile {
|
||||||
file_name,
|
file_name,
|
||||||
machine: image.machine,
|
machine: image.machine,
|
||||||
has_osrel: image.sections.contains_key(".osrel"),
|
// The sections are dropped here, so only the small entries are kept per image.
|
||||||
// The sections are dropped here, so only the small entry is kept per image.
|
profiles: uki_profiles(&image.sections)
|
||||||
entry: BlsEntry::from_uki(&image.sections, &path),
|
.into_iter()
|
||||||
|
.map(|profile| UkiProfileEntry {
|
||||||
|
index: profile.index,
|
||||||
|
has_osrel: profile.sections.contains_key(".osrel"),
|
||||||
|
entry: BlsEntry::from_uki_profile(&profile, &path),
|
||||||
|
info: profile.info,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
}),
|
}),
|
||||||
Err(error) => warn!("unable to read unified kernel image {}: {:#}", path, error),
|
Err(error) => warn!("unable to read unified kernel image {}: {:#}", path, error),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -547,7 +547,7 @@ fn run(reboot_on_error: &mut bool) -> Result<()> {
|
|||||||
let default_flags = edera_sprout_bls::resolve_default_flags(
|
let default_flags = edera_sprout_bls::resolve_default_flags(
|
||||||
&entries
|
&entries
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| (entry.is_default(), entry.is_bad()))
|
.map(|entry| (entry.is_default(), entry.is_bad(), entry.is_extra_profile()))
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
for (entry, default) in entries.iter_mut().zip(default_flags) {
|
for (entry, default) in entries.iter_mut().zip(default_flags) {
|
||||||
|
|||||||
Reference in new issue
Block a user