use crate::formats::initramfs::{detect, Compression};
use crate::formats::osrel::{read_os_release, OsRelease};
use crate::formats::pe::PeFile;
use crate::inspect::ext::SectionLookupExt;
use anyhow::{Context, Result};
use sha2::{Digest, Sha256};
use std::path::PathBuf;
use std::time::Instant;
use tracing::{debug, debug_span};
#[derive(Debug)]
pub struct UkiOptions {
pub file: PathBuf,
}
#[derive(Debug, serde::Serialize)]
pub struct Report {
pub arch: String, pub pe32_plus: bool, pub has_signature: bool, pub cert_count: usize, pub cmdline: String,
pub os_release: Option<OsRelease>,
pub linux: SectionInfo,
pub initrd: InitrdInfo,
}
#[derive(Debug, serde::Serialize)]
pub struct SectionInfo {
pub offset: usize,
pub size: usize,
pub sha256: String,
}
#[derive(Debug, serde::Serialize)]
pub struct InitrdInfo {
#[serde(flatten)]
pub section: SectionInfo,
pub compression: Compression,
#[serde(skip_serializing_if = "Option::is_none")]
pub entries_estimate: Option<usize>,
}
pub fn inspect(UkiOptions { file: uki }: UkiOptions) -> Result<Report> {
let _inspect_span = debug_span!("inspect", path = %uki.display()).entered();
let t0 = Instant::now();
let bytes = std::fs::read(&uki).with_context(|| format!("read {}", uki.display()))?;
debug!(
len = bytes.len(),
elapsed_ms = t0.elapsed().as_millis(),
"read_file"
);
let t = Instant::now();
let pef = PeFile::from_bytes(bytes)?;
let (arch, pe32p) = pef.arch_summary()?;
debug!(
arch,
pe32_plus = pe32p,
elapsed_ms = t.elapsed().as_millis(),
"parse_pe"
);
let t = Instant::now();
let cmdline = pef
.read_text(".cmdline")?
.unwrap_or_default()
.trim()
.to_string();
let os_release: Option<OsRelease> = read_os_release(&pef)?;
debug!(elapsed_ms = t.elapsed().as_millis(), "metadata");
let (mut linux_info, linux_bytes) = pef.section_info_and_bytes(".linux")?;
let t = Instant::now();
linux_info.sha256 = format!("{:x}", Sha256::digest(linux_bytes));
debug!(
size = linux_bytes.len(),
elapsed_ms = t.elapsed().as_millis(),
"sha256_linux"
);
let (mut initrd_info, initrd_bytes) = pef.section_info_and_bytes(".initrd")?;
let t = Instant::now();
initrd_info.sha256 = format!("{:x}", Sha256::digest(initrd_bytes));
let detect_t = Instant::now();
let compression = detect(initrd_bytes);
debug!(
size = initrd_bytes.len(),
hash_ms = t.elapsed().as_millis(),
detect_ms = detect_t.elapsed().as_millis(),
"initrd_hash_and_detect"
);
let t = Instant::now();
let cert_count = pef.certificate_blobs()?.len();
let has_signature = cert_count > 0;
debug!(
cert_count,
elapsed_ms = t.elapsed().as_millis(),
"certificates"
);
let initrd = InitrdInfo {
section: initrd_info,
compression,
entries_estimate: None,
};
Ok(Report {
arch: arch.to_string(),
pe32_plus: pe32p,
has_signature,
cert_count,
cmdline,
os_release,
linux: linux_info,
initrd,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::formats::initramfs::{detect, Compression};
use crate::formats::osrel::read_os_release_from_str;
#[test]
fn initramfs_detects_gzip_xz_zstd_newc_unknown() {
assert!(matches!(
detect(&[0x1F, 0x8B, 0x08, 0x00]),
Compression::Gzip
));
assert!(matches!(
detect(&[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]),
Compression::Xz
));
assert!(matches!(
detect(&[0x28, 0xB5, 0x2F, 0xFD]),
Compression::Zstd
));
assert!(matches!(detect(b"070701..."), Compression::Uncompressed));
assert!(matches!(detect(&[]), Compression::Unknown));
assert!(matches!(detect(&[0x00, 0x01]), Compression::Unknown));
}
#[test]
fn osrelease_parses_fedora41_and_prefers_pretty_name() {
let fedora = r#"NAME="Fedora Linux"
VERSION="41 (Forty One)"
ID=fedora
VERSION_ID=41
PRETTY_NAME="Fedora Linux 41 (Forty One)"
"#;
let os = read_os_release_from_str(fedora)
.expect("parse ok")
.expect("Some(os-release)");
assert_eq!(os.name.as_deref(), Some("Fedora Linux 41 (Forty One)"));
assert_eq!(os.id.as_deref(), Some("fedora"));
assert_eq!(os.version_id.as_deref(), Some("41"));
}
#[test]
fn osrelease_falls_back_to_name_when_pretty_missing() {
let minimal = r#"NAME="MyOS"
ID=myos
VERSION_ID="1.2.3"
"#;
let os = read_os_release_from_str(minimal)
.expect("parse ok")
.expect("Some(os-release)");
assert_eq!(os.name.as_deref(), Some("MyOS"));
assert_eq!(os.id.as_deref(), Some("myos"));
assert_eq!(os.version_id.as_deref(), Some("1.2.3"));
}
#[test]
#[ignore = "requires UKI_PATH"]
fn inspect_real_uki_smoke() {
let uki_path = std::env::var("UKI_PATH").expect("set UKI_PATH to a real UKI");
let report = inspect(UkiOptions {
file: uki_path.into(),
})
.expect("inspect report");
assert!(!report.arch.is_empty());
assert!(report.linux.size > 0);
assert!(report.initrd.section.size > 0);
assert_ne!(report.initrd.compression, Compression::Unknown);
assert_eq!(report.linux.sha256.len(), 64);
assert!(report.linux.sha256.chars().all(|c| c.is_ascii_hexdigit()));
assert_eq!(report.initrd.section.sha256.len(), 64);
assert!(report
.initrd
.section
.sha256
.chars()
.all(|c| c.is_ascii_hexdigit()));
assert_eq!(report.cmdline, report.cmdline.trim());
}
}