lowell_core/inspect/
uki.rs1use crate::formats::initramfs::{detect, Compression};
2use crate::formats::osrel::{read_os_release, OsRelease};
3use crate::formats::pe::PeFile;
4use crate::inspect::ext::SectionLookupExt;
5use anyhow::{Context, Result};
6use sha2::{Digest, Sha256};
7use std::path::PathBuf;
8use std::time::Instant;
9use tracing::{debug, debug_span};
10
11#[derive(Debug)]
12pub struct UkiOptions {
13 pub file: PathBuf,
15}
16
17#[derive(Debug, serde::Serialize)]
18pub struct Report {
19 pub arch: String, pub pe32_plus: bool, pub has_signature: bool, pub cert_count: usize, pub cmdline: String,
24 pub os_release: Option<OsRelease>,
25 pub linux: SectionInfo,
26 pub initrd: InitrdInfo,
27}
28
29#[derive(Debug, serde::Serialize)]
30pub struct SectionInfo {
31 pub offset: usize,
32 pub size: usize,
33 pub sha256: String,
34}
35
36#[derive(Debug, serde::Serialize)]
37pub struct InitrdInfo {
38 #[serde(flatten)]
39 pub section: SectionInfo,
40 pub compression: Compression,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub entries_estimate: Option<usize>,
43}
44
45pub fn inspect(UkiOptions { file: uki }: UkiOptions) -> Result<Report> {
46 let _inspect_span = debug_span!("inspect", path = %uki.display()).entered();
48
49 let t0 = Instant::now();
51 let bytes = std::fs::read(&uki).with_context(|| format!("read {}", uki.display()))?;
52 debug!(
53 len = bytes.len(),
54 elapsed_ms = t0.elapsed().as_millis(),
55 "read_file"
56 );
57
58 let t = Instant::now();
60 let pef = PeFile::from_bytes(bytes)?;
61 let (arch, pe32p) = pef.arch_summary()?;
62 debug!(
63 arch,
64 pe32_plus = pe32p,
65 elapsed_ms = t.elapsed().as_millis(),
66 "parse_pe"
67 );
68
69 let t = Instant::now();
71 let cmdline = pef
72 .read_text(".cmdline")?
73 .unwrap_or_default()
74 .trim()
75 .to_string();
76 let os_release: Option<OsRelease> = read_os_release(&pef)?;
77 debug!(elapsed_ms = t.elapsed().as_millis(), "metadata");
78
79 let (mut linux_info, linux_bytes) = pef.section_info_and_bytes(".linux")?;
81 let t = Instant::now();
82 linux_info.sha256 = format!("{:x}", Sha256::digest(linux_bytes));
83 debug!(
84 size = linux_bytes.len(),
85 elapsed_ms = t.elapsed().as_millis(),
86 "sha256_linux"
87 );
88
89 let (mut initrd_info, initrd_bytes) = pef.section_info_and_bytes(".initrd")?;
91 let t = Instant::now();
92 initrd_info.sha256 = format!("{:x}", Sha256::digest(initrd_bytes));
93 let detect_t = Instant::now();
94 let compression = detect(initrd_bytes);
95 debug!(
96 size = initrd_bytes.len(),
97 hash_ms = t.elapsed().as_millis(),
98 detect_ms = detect_t.elapsed().as_millis(),
99 "initrd_hash_and_detect"
100 );
101
102 let t = Instant::now();
104 let cert_count = pef.certificate_blobs()?.len();
105 let has_signature = cert_count > 0;
106 debug!(
107 cert_count,
108 elapsed_ms = t.elapsed().as_millis(),
109 "certificates"
110 );
111
112 let initrd = InitrdInfo {
113 section: initrd_info,
114 compression,
115 entries_estimate: None,
116 };
117
118 Ok(Report {
119 arch: arch.to_string(),
120 pe32_plus: pe32p,
121 has_signature,
122 cert_count,
123 cmdline,
124 os_release,
125 linux: linux_info,
126 initrd,
127 })
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::formats::initramfs::{detect, Compression};
134 use crate::formats::osrel::read_os_release_from_str;
135
136 #[test]
139 fn initramfs_detects_gzip_xz_zstd_newc_unknown() {
140 assert!(matches!(
142 detect(&[0x1F, 0x8B, 0x08, 0x00]),
143 Compression::Gzip
144 ));
145
146 assert!(matches!(
148 detect(&[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]),
149 Compression::Xz
150 ));
151
152 assert!(matches!(
154 detect(&[0x28, 0xB5, 0x2F, 0xFD]),
155 Compression::Zstd
156 ));
157
158 assert!(matches!(detect(b"070701..."), Compression::Uncompressed));
160
161 assert!(matches!(detect(&[]), Compression::Unknown));
163 assert!(matches!(detect(&[0x00, 0x01]), Compression::Unknown));
164 }
165
166 #[test]
169 fn osrelease_parses_fedora41_and_prefers_pretty_name() {
170 let fedora = r#"NAME="Fedora Linux"
172VERSION="41 (Forty One)"
173ID=fedora
174VERSION_ID=41
175PRETTY_NAME="Fedora Linux 41 (Forty One)"
176"#;
177
178 let os = read_os_release_from_str(fedora)
179 .expect("parse ok")
180 .expect("Some(os-release)");
181
182 assert_eq!(os.name.as_deref(), Some("Fedora Linux 41 (Forty One)"));
184 assert_eq!(os.id.as_deref(), Some("fedora"));
186 assert_eq!(os.version_id.as_deref(), Some("41"));
187 }
188
189 #[test]
190 fn osrelease_falls_back_to_name_when_pretty_missing() {
191 let minimal = r#"NAME="MyOS"
192ID=myos
193VERSION_ID="1.2.3"
194"#;
195 let os = read_os_release_from_str(minimal)
196 .expect("parse ok")
197 .expect("Some(os-release)");
198
199 assert_eq!(os.name.as_deref(), Some("MyOS"));
201 assert_eq!(os.id.as_deref(), Some("myos"));
202 assert_eq!(os.version_id.as_deref(), Some("1.2.3"));
203 }
204
205 #[test]
210 #[ignore = "requires UKI_PATH"]
211 fn inspect_real_uki_smoke() {
212 let uki_path = std::env::var("UKI_PATH").expect("set UKI_PATH to a real UKI");
213 let report = inspect(UkiOptions {
214 file: uki_path.into(),
215 })
216 .expect("inspect report");
217
218 assert!(!report.arch.is_empty());
220 assert!(report.linux.size > 0);
221 assert!(report.initrd.section.size > 0);
222 assert_ne!(report.initrd.compression, Compression::Unknown);
223
224 assert_eq!(report.linux.sha256.len(), 64);
226 assert!(report.linux.sha256.chars().all(|c| c.is_ascii_hexdigit()));
227 assert_eq!(report.initrd.section.sha256.len(), 64);
228 assert!(report
229 .initrd
230 .section
231 .sha256
232 .chars()
233 .all(|c| c.is_ascii_hexdigit()));
234
235 assert_eq!(report.cmdline, report.cmdline.trim());
237 }
238}