#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KernelId {
Path(std::path::PathBuf),
Version(String),
CacheKey(String),
}
impl KernelId {
pub fn parse(s: &str) -> Self {
if s.contains('/') || s.starts_with('.') || s.starts_with('~') {
return KernelId::Path(std::path::PathBuf::from(s));
}
if _is_version_string(s) {
return KernelId::Version(s.to_string());
}
KernelId::CacheKey(s.to_string())
}
}
impl std::fmt::Display for KernelId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
KernelId::Path(p) => write!(f, "{}", p.display()),
KernelId::Version(v) => write!(f, "{v}"),
KernelId::CacheKey(k) => write!(f, "{k}"),
}
}
}
fn _is_version_string(s: &str) -> bool {
let (version_part, rc_part) = match s.split_once("-rc") {
Some((v, rc)) => (v, Some(rc)),
None => (s, None),
};
if let Some(rc) = rc_part
&& (rc.is_empty() || !rc.bytes().all(|b| b.is_ascii_digit()))
{
return false;
}
let mut parts = version_part.split('.');
match parts.next() {
Some(p) if !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()) => {}
_ => return false,
}
match parts.next() {
Some(p) if !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()) => {}
_ => return false,
}
if let Some(patch) = parts.next()
&& (patch.is_empty() || !patch.bytes().all(|b| b.is_ascii_digit()))
{
return false;
}
parts.next().is_none()
}
#[allow(dead_code)]
pub fn resolve_kernel(kernel_dir: Option<&str>) -> Option<std::path::PathBuf> {
if let Some(dir) = kernel_dir {
let p = std::path::PathBuf::from(dir);
if p.is_dir() {
return Some(p);
}
}
for rel in &["./linux", "../linux"] {
let p = std::path::PathBuf::from(rel);
if p.is_dir() && _has_kernel_artifacts(&p) {
return Some(p);
}
}
if let Some(rel) = _kernel_release() {
let p = std::path::PathBuf::from(format!("/lib/modules/{rel}/build"));
if p.is_dir() {
return Some(p);
}
}
None
}
#[allow(dead_code)]
pub fn find_image_in_dir(dir: &std::path::Path) -> Option<std::path::PathBuf> {
#[cfg(target_arch = "x86_64")]
{
let p = dir.join("arch/x86/boot/bzImage");
if p.exists() {
return Some(p);
}
}
#[cfg(target_arch = "aarch64")]
{
let p = dir.join("arch/arm64/boot/Image");
if p.exists() {
return Some(p);
}
}
#[cfg(target_arch = "x86_64")]
{
let p = dir.join("bzImage");
if p.exists() {
return Some(p);
}
}
#[cfg(target_arch = "aarch64")]
{
let p = dir.join("Image");
if p.exists() {
return Some(p);
}
}
None
}
#[allow(dead_code)]
pub fn find_image(kernel_dir: Option<&str>, release: Option<&str>) -> Option<std::path::PathBuf> {
if let Some(dir_str) = kernel_dir {
let dir = std::path::PathBuf::from(dir_str);
if !dir.is_dir() {
return None;
}
return find_image_in_dir(&dir);
}
if let Some(dir) = resolve_kernel(None)
&& let Some(img) = find_image_in_dir(&dir)
{
return Some(img);
}
let owned_release;
let rel = match release {
Some(r) => Some(r),
None => {
owned_release = _kernel_release();
owned_release.as_deref()
}
};
if let Some(rel) = rel {
let p = std::path::PathBuf::from(format!("/lib/modules/{rel}/vmlinuz"));
if std::fs::File::open(&p).is_ok() {
return Some(p);
}
let p = std::path::PathBuf::from(format!("/boot/vmlinuz-{rel}"));
if std::fs::File::open(&p).is_ok() {
return Some(p);
}
}
let p = std::path::PathBuf::from("/boot/vmlinuz");
if std::fs::File::open(&p).is_ok() {
return Some(p);
}
None
}
#[allow(dead_code)]
pub fn resolve_btf(kernel_dir: Option<&str>) -> Option<std::path::PathBuf> {
if let Some(dir) = resolve_kernel(kernel_dir) {
let vmlinux = dir.join("vmlinux");
if vmlinux.exists() {
return Some(vmlinux);
}
}
let sysfs = std::path::Path::new("/sys/kernel/btf/vmlinux");
if sysfs.exists() {
return Some(sysfs.to_path_buf());
}
None
}
#[allow(dead_code)]
fn _has_kernel_artifacts(dir: &std::path::Path) -> bool {
if dir.join("vmlinux").exists() {
return true;
}
#[cfg(target_arch = "x86_64")]
if dir.join("arch/x86/boot/bzImage").exists() || dir.join("bzImage").exists() {
return true;
}
#[cfg(target_arch = "aarch64")]
if dir.join("arch/arm64/boot/Image").exists() || dir.join("Image").exists() {
return true;
}
false
}
#[allow(dead_code)]
fn _kernel_release() -> Option<String> {
std::process::Command::new("uname")
.arg("-r")
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use tempfile::TempDir;
#[test]
fn kernel_path_resolve_explicit_dir_exists() {
let tmp = TempDir::new().unwrap();
let result = resolve_kernel(Some(tmp.path().to_str().unwrap()));
assert_eq!(result, Some(tmp.path().to_path_buf()));
}
#[test]
fn kernel_path_resolve_explicit_dir_not_exists() {
let result = resolve_kernel(Some("/nonexistent/kernel/dir/that/does/not/exist"));
assert_ne!(
result,
Some(PathBuf::from("/nonexistent/kernel/dir/that/does/not/exist"))
);
}
#[test]
fn kernel_path_resolve_none_falls_through() {
let _ = resolve_kernel(None);
}
#[test]
fn kernel_path_resolve_empty_string() {
let result = resolve_kernel(Some(""));
assert_ne!(result, Some(PathBuf::from("")));
}
#[test]
fn kernel_path_has_artifacts_vmlinux() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("vmlinux"), b"fake").unwrap();
assert!(_has_kernel_artifacts(tmp.path()));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn kernel_path_has_artifacts_bzimage() {
let tmp = TempDir::new().unwrap();
let boot = tmp.path().join("arch/x86/boot");
std::fs::create_dir_all(&boot).unwrap();
std::fs::write(boot.join("bzImage"), b"fake").unwrap();
assert!(_has_kernel_artifacts(tmp.path()));
}
#[test]
fn kernel_path_has_artifacts_empty_dir() {
let tmp = TempDir::new().unwrap();
assert!(!_has_kernel_artifacts(tmp.path()));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn kernel_path_find_image_in_dir_bzimage() {
let tmp = TempDir::new().unwrap();
let boot = tmp.path().join("arch/x86/boot");
std::fs::create_dir_all(&boot).unwrap();
std::fs::write(boot.join("bzImage"), b"fake").unwrap();
let result = find_image_in_dir(tmp.path());
assert_eq!(result, Some(boot.join("bzImage")));
}
#[test]
fn kernel_path_find_image_in_dir_empty() {
let tmp = TempDir::new().unwrap();
assert!(find_image_in_dir(tmp.path()).is_none());
}
#[cfg(target_arch = "x86_64")]
#[test]
fn kernel_path_find_image_in_dir_cache_layout() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("bzImage"), b"fake").unwrap();
let result = find_image_in_dir(tmp.path());
assert_eq!(result, Some(tmp.path().join("bzImage")));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn kernel_path_find_image_in_dir_prefers_build_tree() {
let tmp = TempDir::new().unwrap();
let boot = tmp.path().join("arch/x86/boot");
std::fs::create_dir_all(&boot).unwrap();
std::fs::write(boot.join("bzImage"), b"build-tree").unwrap();
std::fs::write(tmp.path().join("bzImage"), b"root-level").unwrap();
let result = find_image_in_dir(tmp.path());
assert_eq!(result, Some(boot.join("bzImage")));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn kernel_path_has_artifacts_root_bzimage() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("bzImage"), b"fake").unwrap();
assert!(_has_kernel_artifacts(tmp.path()));
}
#[test]
fn kernel_path_resolve_btf_with_vmlinux_in_dir() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("vmlinux"), b"fake").unwrap();
let result = resolve_btf(Some(tmp.path().to_str().unwrap()));
assert_eq!(result, Some(tmp.path().join("vmlinux")));
}
#[test]
fn kernel_path_resolve_btf_dir_without_vmlinux() {
let tmp = TempDir::new().unwrap();
let result = resolve_btf(Some(tmp.path().to_str().unwrap()));
if let Some(ref p) = result {
assert!(p.exists());
}
}
#[test]
fn kernel_path_resolve_btf_nonexistent_dir() {
let result = resolve_btf(Some("/nonexistent/btf/dir/xyz"));
if let Some(ref p) = result {
assert!(p.exists());
}
}
#[cfg(target_arch = "x86_64")]
#[test]
fn kernel_path_find_image_explicit_dir_with_bzimage() {
let tmp = TempDir::new().unwrap();
let boot = tmp.path().join("arch/x86/boot");
std::fs::create_dir_all(&boot).unwrap();
std::fs::write(boot.join("bzImage"), b"fake").unwrap();
let result = find_image(Some(tmp.path().to_str().unwrap()), None);
assert_eq!(result, Some(boot.join("bzImage")));
}
#[test]
fn kernel_path_find_image_nonexistent_dir() {
let _ = find_image(Some("/nonexistent/image/dir/xyz"), None);
}
#[test]
fn kernel_path_kernel_release_returns_string() {
let rel = _kernel_release();
assert!(rel.is_some());
let s = rel.unwrap();
assert!(!s.is_empty());
assert!(!s.contains('\n'));
}
#[test]
fn kernel_id_parse_path_with_slash() {
assert_eq!(
KernelId::parse("../linux"),
KernelId::Path(PathBuf::from("../linux"))
);
assert_eq!(
KernelId::parse("/boot/vmlinuz"),
KernelId::Path(PathBuf::from("/boot/vmlinuz"))
);
}
#[test]
fn kernel_id_parse_path_dot_prefix() {
assert_eq!(
KernelId::parse("./linux"),
KernelId::Path(PathBuf::from("./linux"))
);
assert_eq!(KernelId::parse("."), KernelId::Path(PathBuf::from(".")));
}
#[test]
fn kernel_id_parse_path_tilde_prefix() {
assert_eq!(
KernelId::parse("~/linux"),
KernelId::Path(PathBuf::from("~/linux"))
);
}
#[test]
fn kernel_id_parse_version_stable() {
assert_eq!(
KernelId::parse("6.14.2"),
KernelId::Version("6.14.2".to_string())
);
assert_eq!(
KernelId::parse("6.14"),
KernelId::Version("6.14".to_string())
);
}
#[test]
fn kernel_id_parse_version_rc() {
assert_eq!(
KernelId::parse("6.15-rc3"),
KernelId::Version("6.15-rc3".to_string())
);
}
#[test]
fn kernel_id_parse_version_patch_rc() {
assert_eq!(
KernelId::parse("6.14.2-rc1"),
KernelId::Version("6.14.2-rc1".to_string())
);
}
#[test]
fn kernel_id_parse_cache_key() {
assert_eq!(
KernelId::parse("6.14.2-tarball-x86_64"),
KernelId::CacheKey("6.14.2-tarball-x86_64".to_string())
);
assert_eq!(
KernelId::parse("local-deadbeef-x86_64"),
KernelId::CacheKey("local-deadbeef-x86_64".to_string())
);
}
#[test]
fn kernel_id_parse_v_prefix_not_version() {
assert_eq!(
KernelId::parse("v6.14"),
KernelId::CacheKey("v6.14".to_string())
);
}
#[test]
fn kernel_id_parse_bare_major_not_version() {
assert_eq!(KernelId::parse("6"), KernelId::CacheKey("6".to_string()));
}
#[test]
fn kernel_id_display() {
assert_eq!(
KernelId::Version("6.14.2".to_string()).to_string(),
"6.14.2"
);
assert_eq!(
KernelId::Path(PathBuf::from("../linux")).to_string(),
"../linux"
);
assert_eq!(
KernelId::CacheKey("my-key".to_string()).to_string(),
"my-key"
);
}
#[test]
fn kernel_id_is_version_string_valid() {
assert!(_is_version_string("6.14"));
assert!(_is_version_string("6.14.2"));
assert!(_is_version_string("6.15-rc3"));
assert!(_is_version_string("6.14.0-rc1"));
assert!(_is_version_string("5.0"));
assert!(_is_version_string("5.0.0"));
assert!(_is_version_string("5.4.0"));
}
#[test]
fn kernel_id_is_version_string_invalid() {
assert!(!_is_version_string("6"));
assert!(!_is_version_string("v6.14"));
assert!(!_is_version_string(""));
assert!(!_is_version_string("6.14.2-tarball-x86_64"));
assert!(!_is_version_string("6.14.2.3"));
assert!(!_is_version_string("6.14-rc"));
assert!(!_is_version_string("6.14-rcX"));
assert!(!_is_version_string("6.14-rc3-tarball-x86_64"));
assert!(!_is_version_string("abc"));
assert!(!_is_version_string(".14"));
assert!(!_is_version_string("6."));
assert!(!_is_version_string("linux"));
assert!(!_is_version_string(".6"));
}
proptest::proptest! {
#[test]
fn prop_kernel_id_parse_never_panics(s in "\\PC{0,30}") {
let _ = KernelId::parse(&s);
}
#[test]
fn prop_kernel_id_path_on_slash(
prefix in "[a-z]{1,5}",
suffix in "[a-z]{1,5}",
) {
let s = format!("{prefix}/{suffix}");
assert!(matches!(KernelId::parse(&s), KernelId::Path(_)));
}
#[test]
fn prop_kernel_id_path_on_dot_prefix(s in "\\.[a-z]{1,10}") {
assert!(matches!(KernelId::parse(&s), KernelId::Path(_)));
}
#[test]
fn prop_kernel_id_version_roundtrip(
major in 1u32..20,
minor in 0u32..50,
patch in 0u32..100,
) {
let v = format!("{major}.{minor}.{patch}");
assert_eq!(KernelId::parse(&v), KernelId::Version(v.clone()));
}
#[test]
fn prop_kernel_id_version_rc(major in 1u32..20, minor in 0u32..50, rc in 1u32..10) {
let v = format!("{major}.{minor}-rc{rc}");
assert_eq!(KernelId::parse(&v), KernelId::Version(v.clone()));
}
}
}