use std::{collections::BTreeMap, fmt};
#[cfg(feature = "cli")]
use std::path::{Path, PathBuf};
#[cfg(feature = "cli")]
use anyhow::Context;
use serde::{
Deserialize, Deserializer,
de::{Error, Visitor},
};
use crate::abi::{
FlatCStr, MAX_ENGINE_NAME_LEN, MAX_ENV_ENTRIES, MAX_ENV_ENTRY_LEN, MAX_PATH_LEN,
MAX_SUBNET_LEN, SandboxSafe,
};
use crate::engine::{BARYL_FW_BIOS, BARYL_FW_UEFI};
use crate::enlighten::{BARYL_OS_LINUX, BARYL_OS_WINDOWS};
type FlatPath = FlatCStr<MAX_PATH_LEN>;
type FlatEnvEntry = FlatCStr<MAX_ENV_ENTRY_LEN>;
#[repr(C)]
#[derive(Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MachineConfig {
pub baryl: BarylSection,
pub machine: MachineSection,
#[serde(default)]
pub firmware: FirmwareSection,
#[serde(default)]
pub network: NetworkSection,
#[serde(default)]
pub env: EnvTable,
}
unsafe impl SandboxSafe for MachineConfig {}
const _: () = {
assert!(size_of::<MachineConfig>() == 0x2458);
};
impl MachineConfig {
pub fn engine(&self) -> &str {
self.baryl.engine.as_str()
}
}
#[repr(C)]
#[derive(Clone, Copy, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BarylSection {
pub engine: FlatCStr<MAX_ENGINE_NAME_LEN>,
}
#[repr(C)]
#[derive(Clone, Copy, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MachineSection {
#[serde(default = "ram_mib_default")]
pub ram_mib: u64,
#[serde(skip)]
pub image: FlatPath,
#[serde(default)]
pub floppy: FlatPath,
#[serde(deserialize_with = "guest_os_word_parse")]
pub guest_os: u32,
#[serde(default, deserialize_with = "serial_flag_parse")]
pub serial: u8,
}
#[repr(C)]
#[derive(Clone, Copy, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FirmwareSection {
#[serde(
default = "firmware_kind_default",
deserialize_with = "firmware_kind_parse"
)]
pub kind: u32,
#[serde(default)]
pub vars: FlatPath,
}
impl Default for FirmwareSection {
fn default() -> FirmwareSection {
FirmwareSection {
kind: BARYL_FW_BIOS,
vars: FlatPath::empty(),
}
}
}
#[repr(C)]
#[derive(Clone, Copy, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NetworkSection {
#[serde(default)]
pub subnet: FlatCStr<MAX_SUBNET_LEN>,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct EnvTable {
pub entries: [FlatEnvEntry; MAX_ENV_ENTRIES],
pub count: u32,
}
impl Default for EnvTable {
fn default() -> EnvTable {
EnvTable {
entries: [FlatEnvEntry::empty(); MAX_ENV_ENTRIES],
count: 0,
}
}
}
impl EnvTable {
pub fn as_slice(&self) -> &[FlatEnvEntry] {
&self.entries[..self.count as usize]
}
}
impl<'de> Deserialize<'de> for EnvTable {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<EnvTable, D::Error> {
let table: BTreeMap<String, EnvEntryValue> = BTreeMap::deserialize(d)?;
if table.len() > MAX_ENV_ENTRIES {
return Err(D::Error::custom(format!(
"[env] has {} entries, over MAX_ENV_ENTRIES ({MAX_ENV_ENTRIES})",
table.len()
)));
}
let mut entries = [FlatEnvEntry::empty(); MAX_ENV_ENTRIES];
for (slot, (k, v)) in entries.iter_mut().zip(&table) {
let entry = format!("{k}={}", v.0);
*slot = FlatEnvEntry::try_new(&entry).ok_or_else(|| {
D::Error::custom(format!("[env] entry exceeds MAX_ENV_ENTRY_LEN: {entry}"))
})?;
}
Ok(EnvTable {
entries: entries,
count: table.len() as u32,
})
}
}
struct EnvEntryValue(String);
impl<'de> Deserialize<'de> for EnvEntryValue {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<EnvEntryValue, D::Error> {
d.deserialize_any(EnvEntryValueVisitor)
}
}
struct EnvEntryValueVisitor;
impl Visitor<'_> for EnvEntryValueVisitor {
type Value = EnvEntryValue;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a string, integer, float or boolean")
}
fn visit_str<E: Error>(self, v: &str) -> Result<EnvEntryValue, E> {
Ok(EnvEntryValue(v.to_string()))
}
fn visit_i64<E: Error>(self, v: i64) -> Result<EnvEntryValue, E> {
Ok(EnvEntryValue(v.to_string()))
}
fn visit_u64<E: Error>(self, v: u64) -> Result<EnvEntryValue, E> {
Ok(EnvEntryValue(v.to_string()))
}
fn visit_f64<E: Error>(self, v: f64) -> Result<EnvEntryValue, E> {
Ok(EnvEntryValue(v.to_string()))
}
fn visit_bool<E: Error>(self, v: bool) -> Result<EnvEntryValue, E> {
Ok(EnvEntryValue(v.to_string()))
}
}
fn guest_os_word_parse<'de, D: Deserializer<'de>>(d: D) -> Result<u32, D::Error> {
match String::deserialize(d)?.as_str() {
"linux" => Ok(BARYL_OS_LINUX),
"windows" => Ok(BARYL_OS_WINDOWS),
other => Err(D::Error::custom(format!(
"unknown guest os: {other} (expected 'linux' or 'windows')"
))),
}
}
fn firmware_kind_parse<'de, D: Deserializer<'de>>(d: D) -> Result<u32, D::Error> {
match String::deserialize(d)?.as_str() {
"bios" => Ok(BARYL_FW_BIOS),
"uefi" => Ok(BARYL_FW_UEFI),
other => Err(D::Error::custom(format!(
"unknown firmware kind: {other} (expected 'bios' or 'uefi')"
))),
}
}
fn firmware_kind_default() -> u32 {
BARYL_FW_BIOS
}
pub const RAM_MIB_DEFAULT: u64 = 4096;
fn ram_mib_default() -> u64 {
RAM_MIB_DEFAULT
}
fn serial_flag_parse<'de, D: Deserializer<'de>>(d: D) -> Result<u8, D::Error> {
Ok(u8::from(bool::deserialize(d)?))
}
#[cfg(feature = "cli")]
impl MachineConfig {
pub fn path_beside(image: &Path) -> PathBuf {
let mut p = image.as_os_str().to_owned();
p.push(".toml");
PathBuf::from(p)
}
pub fn template_build(engine: &str) -> String {
format!(
"# Written by `baryl configure`. Commented keys show their defaults.\n\
\n\
[baryl]\n\
engine = \"{engine}\"\n\
\n\
[machine]\n\
# The guest kernel; picks the enlighten subsystem.\n\
guest_os = \"linux\"\n\
# ram_mib = {RAM_MIB_DEFAULT}\n\
# serial = false\n\
# floppy = \"seed.img\"\n\
\n\
# [firmware]\n\
# kind = \"bios\" # bios | uefi\n\
# vars = \"OVMF_VARS.fd\"\n\
\n\
# [network]\n\
# subnet = \"10.0.2.0/24\"\n\
\n\
# Passed to the engine verbatim.\n\
# [env]\n"
)
}
pub fn read(path: &Path) -> anyhow::Result<MachineConfig> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("read the machine config {}", path.display()))?;
let cfg: MachineConfig = toml::from_str(&text)
.with_context(|| format!("parse the machine config {}", path.display()))?;
let dir = path.parent().unwrap_or_else(|| Path::new("."));
Ok(MachineConfig {
machine: MachineSection {
floppy: config_path_rebase(&cfg.machine.floppy, dir)?,
..cfg.machine
},
firmware: FirmwareSection {
vars: config_path_rebase(&cfg.firmware.vars, dir)?,
..cfg.firmware
},
..cfg
})
}
}
#[cfg(feature = "cli")]
fn config_path_rebase(p: &FlatPath, dir: &Path) -> anyhow::Result<FlatPath> {
if p.is_empty() {
return Ok(FlatPath::empty());
}
let joined = dir.join(p.as_str());
let s = joined
.to_str()
.context("machine config names a non-UTF-8 path")?;
FlatPath::try_new(s).with_context(|| format!("path exceeds {MAX_PATH_LEN} bytes: {s}"))
}