use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct OsInfo {
#[serde(default)]
pub display_name: Option<String>,
pub name: String,
pub publisher: String,
pub release_date: String,
pub architecture: String,
#[serde(default)]
pub blurb: OsBlurb,
#[serde(default)]
pub fun_facts: Vec<String>,
#[serde(default)]
pub install_steps: Vec<InstallStep>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct OsBlurb {
pub short: String,
#[serde(default)]
pub long: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstallStep {
pub number: u32,
pub title: String,
pub instructions: String,
#[serde(default)]
pub boot_mode: String,
#[serde(default)]
pub completion_hint: String,
}
#[derive(Debug, Clone, Default)]
pub struct MetadataStore {
pub entries: HashMap<String, OsInfo>,
}
impl MetadataStore {
pub fn load_from_dir(dir: &Path) -> Result<Self> {
let mut store = Self::default();
if !dir.exists() {
return Ok(store);
}
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().map(|e| e == "toml").unwrap_or(false) {
if let Ok(content) = std::fs::read_to_string(&path) {
if let Ok(info) = toml::from_str::<OsInfo>(&content) {
let id = path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
store.entries.insert(id, info);
}
}
}
}
Ok(store)
}
pub fn load_embedded() -> Self {
let mut store = Self::default();
let defaults = include_str!("../../assets/metadata/defaults.toml");
if let Ok(entries) = toml::from_str::<HashMap<String, OsInfo>>(defaults) {
store.entries = entries;
}
store
}
pub fn get(&self, id: &str) -> Option<&OsInfo> {
if let Some(info) = self.entries.get(id) {
return Some(info);
}
if let Some(base_id) = strip_numeric_suffix(id) {
return self.entries.get(base_id);
}
None
}
pub fn merge(&mut self, overrides: MetadataStore) {
for (id, info) in overrides.entries {
self.entries.insert(id, info);
}
}
}
pub fn default_os_info(vm_id: &str) -> OsInfo {
let display_name = vm_id
.replace('-', " ")
.split_whitespace()
.map(|word| {
let mut chars: Vec<char> = word.chars().collect();
if let Some(first) = chars.first_mut() {
*first = first.to_ascii_uppercase();
}
chars.into_iter().collect::<String>()
})
.collect::<Vec<_>>()
.join(" ");
let (publisher, architecture) = guess_os_details(vm_id);
OsInfo {
display_name: None, name: display_name,
publisher,
architecture,
release_date: String::new(),
blurb: OsBlurb::default(),
fun_facts: Vec::new(),
install_steps: Vec::new(),
}
}
pub fn strip_numeric_suffix(id: &str) -> Option<&str> {
if let Some(last_dash) = id.rfind('-') {
let suffix = &id[last_dash + 1..];
if !suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_digit()) {
return Some(&id[..last_dash]);
}
}
None
}
fn guess_os_details(vm_id: &str) -> (String, String) {
let id = vm_id.to_lowercase();
if id.contains("windows") {
let arch = if id.contains("11") || id.contains("10") || id.contains("8") || id.contains("7") {
"x86_64"
} else {
"i386"
};
("Microsoft Corporation".to_string(), arch.to_string())
} else if id.contains("mac") || id.contains("osx") {
let arch = if id.contains("tiger") || id.contains("leopard") {
"PowerPC"
} else if id.contains("system") || id.contains("os-9") {
"Motorola 68k"
} else {
"x86_64"
};
("Apple Inc.".to_string(), arch.to_string())
} else if id.contains("linux") || id.contains("fedora") || id.contains("ubuntu") {
("Various".to_string(), "x86_64".to_string())
} else if id.contains("dos") {
("Microsoft Corporation".to_string(), "i386".to_string())
} else {
("Unknown".to_string(), "Unknown".to_string())
}
}