axbuild 0.5.0

An OS build lib toolkit used by arceos
use std::{mem, path::Path};

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct DriveArg {
    options: QemuOptions,
}

impl DriveArg {
    pub(super) fn parse(value: &str) -> Self {
        Self {
            options: QemuOptions::parse(value),
        }
    }

    pub(super) fn file(&self) -> Option<&str> {
        self.options.value("file")
    }

    pub(super) fn id(&self) -> Option<&str> {
        self.options.value("id")
    }

    pub(super) fn interface(&self) -> Option<&str> {
        self.options.value("if")
    }

    pub(super) fn set_file(&mut self, path: &Path) {
        self.options.set_value("file", &path.display().to_string());
    }

    pub(super) fn snapshot_conflict(&self) -> Option<&str> {
        self.options.value_other_than("snapshot", "off")
    }

    pub(super) fn set_snapshot_on(&mut self) {
        self.options.set_value("snapshot", "on");
    }

    pub(super) fn is_file_backed_block_drive(&self) -> bool {
        self.file().is_some_and(|file| !file.starts_with("fat:"))
            && self.interface() != Some("pflash")
    }

    pub(super) fn render(&self) -> String {
        self.options.render()
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct DeviceArg {
    options: QemuOptions,
}

impl DeviceArg {
    pub(super) fn parse(value: &str) -> Self {
        Self {
            options: QemuOptions::parse(value),
        }
    }

    pub(super) fn drive(&self) -> Option<&str> {
        self.options.value("drive")
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct QemuOptions {
    fields: Vec<String>,
}

impl QemuOptions {
    fn parse(value: &str) -> Self {
        let mut fields = Vec::new();
        let mut field = String::new();
        let mut chars = value.chars().peekable();

        while let Some(character) = chars.next() {
            if character != ',' {
                field.push(character);
                continue;
            }
            if chars.peek() == Some(&',') {
                chars.next();
                field.push(',');
                continue;
            }
            fields.push(mem::take(&mut field));
        }
        fields.push(field);

        Self { fields }
    }

    fn value(&self, key: &str) -> Option<&str> {
        self.fields.iter().find_map(|field| {
            let (field_key, value) = field.split_once('=')?;
            (field_key == key).then_some(value)
        })
    }

    fn value_other_than(&self, key: &str, allowed: &str) -> Option<&str> {
        self.fields.iter().find_map(|field| {
            let (field_key, value) = field.split_once('=')?;
            (field_key == key && value != allowed).then_some(value)
        })
    }

    fn set_value(&mut self, key: &str, value: &str) {
        let mut replaced = false;
        for field in &mut self.fields {
            if field
                .split_once('=')
                .is_some_and(|(field_key, _)| field_key == key)
            {
                *field = format!("{key}={value}");
                replaced = true;
            }
        }
        if !replaced {
            self.fields.push(format!("{key}={value}"));
        }
    }

    fn render(&self) -> String {
        self.fields
            .iter()
            .map(|field| field.replace(',', ",,"))
            .collect::<Vec<_>>()
            .join(",")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn drive_arg_decodes_and_encodes_qemu_escaped_commas() {
        let mut drive = DriveArg::parse(
            "cache=none,file=/tmp/rootfs,,old.img,id=disk0,serial=name,,with,,commas",
        );

        assert_eq!(drive.file(), Some("/tmp/rootfs,old.img"));
        assert_eq!(drive.id(), Some("disk0"));

        drive.set_file(Path::new("/tmp/rootfs,new.img"));
        assert_eq!(
            drive.render(),
            "cache=none,file=/tmp/rootfs,,new.img,id=disk0,serial=name,,with,,commas"
        );
    }
}