patchouly-build 0.1.0

Utility to extract and generate ready-to-use patchouly stencils.
Documentation
use std::{error::Error, fs::File, io::Write, path::Path};

use crate::extract::Extraction;

pub fn generate(extraction: Extraction, dest: &Path) -> Result<(), Box<dyn Error>> {
    let out_bin = dest.join(format!(
        "{}_stencils.bin",
        extraction.lib_name.to_lowercase()
    ));
    {
        let mut out_bin = File::options()
            .create(true)
            .truncate(true)
            .write(true)
            .open(&out_bin)?;
        out_bin.write_all(&extraction.all_code)?;
    }

    {
        let out_rs = dest.join(format!(
            "{}_stencils.rs",
            extraction.lib_name.to_lowercase()
        ));
        let mut out_rs = File::options()
            .create(true)
            .truncate(true)
            .write(true)
            .open(&out_rs)?;

        let lib_upper = extraction.lib_name.to_uppercase();

        out_rs.write_all(
            r#"/// Generated by patchouly-build
mod stencils {

use patchouly_core::StencilLibrary;
use patchouly_core::stencils::{Stencil, StencilFamily};
use patchouly_core::relocation::Relocation;
"#
            .as_bytes(),
        )?;

        out_rs.write_fmt(format_args!(
            r#"
pub const {}_STENCIL_LIBRARY: StencilLibrary<{}> = StencilLibrary {{
    code: include_bytes!({}),
    empty: b"{}",
    moves: &{}___MOVE,
}};"#,
            lib_upper,
            extraction.max_regs,
            escape_string(&out_bin.display().to_string()),
            extraction
                .families
                .get("__empty")
                .and_then(|empty| {
                    if empty.stencils.len() != 1 {
                        return None;
                    }
                    let stencil = empty.stencils[0];
                    Some(escape_binary(stencil.code(&extraction.all_code)))
                })
                .unwrap_or("".to_string()),
            lib_upper,
        ))?;

        // We directly use from_bits/into_bits here.
        // I don't think the generated code will run across multiple arches...
        // TODO: Or do they? Like, when cross-compiling?
        let endian = cfg!(target_endian = "little");
        out_rs.write_fmt(format_args!(
            r#"
// endianess assertion
const _: [u8; 1] = [0; (cfg!(target_endian = "little") == {}) as usize];"#,
            endian
        ))?;

        for (name, stencil) in extraction.families {
            if name == "__empty" {
                continue;
            }

            out_rs.write_fmt(format_args!(
                r#"
pub const {}_{}: StencilFamily<{}, {}, {}, {}, {}> = StencilFamily {{
    relocation_data: &["#,
                lib_upper,
                name.to_uppercase(),
                stencil.IN,
                stencil.OUT,
                stencil.MAX_REGS,
                stencil.HOLES,
                stencil.JUMPS,
            ))?;

            for reloc in &stencil.relocation_data {
                out_rs.write_fmt(format_args!(
                    r#"
        Relocation::from_bits(0x{:x}),"#,
                    reloc.into_bits()
                ))?;
            }

            out_rs.write_all(
                r#"
    ],
    stencils: &["#
                    .as_bytes(),
            )?;

            for stencil in &stencil.stencils {
                out_rs.write_fmt(format_args!(
                    r#"
        Stencil::from_bits(0x{:x}),"#,
                    stencil.into_bits()
                ))?;
            }

            out_rs.write_all(
                r#"
    ],
};
"#
                .as_bytes(),
            )?;
        }

        out_rs.write_all(b"} // mod stencils\n")?;
    }

    Ok(())
}

fn escape_string(s: &str) -> String {
    let hashes = "#".repeat(s.chars().filter(|c| *c == '#').count() + 1);
    format!("r{}\"{}\"{}", hashes, s, hashes)
}

fn escape_binary(s: &[u8]) -> String {
    s.iter().map(|b| format!("\\x{:02x}", b)).collect()
}

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

    #[test]
    fn test_escape_string() {
        assert_eq!(r#####"r##"foo#bar"##"#####, escape_string("foo#bar"));
    }

    #[test]
    fn test_escape_binary() {
        assert_eq!(r#"\x00\x01\x02"#, escape_binary(&[0, 1, 2]));
    }
}