extern crate alloc;
use alloc::format;
use alloc::string::String;
use core::fmt::Write as _;
use crate::clsid::Clsid;
#[derive(Copy, Clone, Debug)]
pub struct InfConfig {
pub provider: &'static str,
pub friendly_name: &'static str,
pub clsid: Clsid,
pub dll_filename: &'static str,
pub major_version: u32,
pub minor_version: u32,
pub driver_date: &'static str,
}
#[must_use]
pub fn generate(config: &InfConfig) -> String {
let mut out = String::with_capacity(2048);
let _ = writeln!(
out,
"; Generated by tympan-apo — INF for {}",
config.friendly_name
);
out.push_str(";\n");
out.push_str("; This is a starting template. Real deployments typically need\n");
out.push_str("; hand-tuning of [Manufacturer] flags, CatalogFile, and the\n");
out.push_str("; install-time co-installer chain.\n\n");
out.push_str("[Version]\n");
out.push_str("Signature = \"$WINDOWS NT$\"\n");
out.push_str("Class = MEDIA\n");
out.push_str("ClassGuid = {4d36e96c-e325-11ce-bfc1-08002be10318}\n");
out.push_str("Provider = %ProviderName%\n");
let _ = writeln!(
out,
"DriverVer = {},{}.{}.0.0",
config.driver_date, config.major_version, config.minor_version
);
out.push('\n');
out.push_str("[DestinationDirs]\n");
out.push_str("DefaultDestDir = 11\n");
out.push('\n');
out.push_str("[SourceDisksNames]\n");
out.push_str("1 = %DiskName%\n");
out.push('\n');
out.push_str("[SourceDisksFiles]\n");
let _ = writeln!(out, "{} = 1", config.dll_filename);
out.push_str("[Manufacturer]\n");
out.push_str("%ProviderName% = ApoModels, NTamd64\n\n");
out.push_str("[ApoModels.NTamd64]\n");
let _ = writeln!(
out,
"%ModelName% = ApoInstall, ROOT\\{}",
clsid_id_string(&config.clsid)
);
out.push_str("[ApoInstall]\n");
out.push_str("CopyFiles = ApoFiles\n");
out.push_str("AddReg = ApoAddReg\n\n");
out.push_str("[ApoFiles]\n");
let _ = writeln!(out, "{}", config.dll_filename);
let clsid_brace = format!("{{{}}}", clsid_no_braces(&config.clsid));
out.push_str("[ApoAddReg]\n");
let _ = writeln!(out, "HKCR,CLSID\\{},,0,%ModelName%", clsid_brace);
let _ = writeln!(
out,
"HKCR,CLSID\\{}\\InprocServer32,,0,\"%11%\\{}\"",
clsid_brace, config.dll_filename
);
let _ = writeln!(
out,
"HKCR,CLSID\\{}\\InprocServer32,ThreadingModel,0,\"Both\"",
clsid_brace
);
out.push_str("[Strings]\n");
let _ = writeln!(out, "ProviderName = \"{}\"", escape_inf(config.provider));
let _ = writeln!(
out,
"ModelName = \"{}\"",
escape_inf(config.friendly_name)
);
out.push_str("DiskName = \"tympan-apo cdylib\"\n");
out
}
fn clsid_id_string(c: &Clsid) -> String {
let mut s = String::with_capacity(32);
let _ = write!(s, "{:08X}", c.data1);
let _ = write!(s, "{:04X}", c.data2);
let _ = write!(s, "{:04X}", c.data3);
for b in c.data4 {
let _ = write!(s, "{b:02X}");
}
s
}
fn clsid_no_braces(c: &Clsid) -> String {
let full = format!("{c}");
full.trim_start_matches('{').trim_end_matches('}').into()
}
fn escape_inf(s: &str) -> String {
s.replace('"', "\"\"")
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_config() -> InfConfig {
InfConfig {
provider: "tympan-apo",
friendly_name: "Test APO",
clsid: Clsid::from_u128(0x12345678_1234_5678_1234_567812345678),
dll_filename: "test_apo.dll",
major_version: 1,
minor_version: 0,
driver_date: "01/01/2026",
}
}
#[test]
fn generated_inf_contains_required_sections() {
let inf = generate(&sample_config());
for section in [
"[Version]",
"[DestinationDirs]",
"[SourceDisksNames]",
"[SourceDisksFiles]",
"[Manufacturer]",
"[ApoInstall]",
"[ApoFiles]",
"[ApoAddReg]",
"[Strings]",
] {
assert!(
inf.contains(section),
"missing section {section} in generated INF:\n{inf}"
);
}
}
#[test]
fn generated_inf_embeds_clsid_and_dll_filename() {
let inf = generate(&sample_config());
assert!(
inf.contains("CLSID\\{12345678-1234-5678-1234-567812345678}"),
"expected brace-delimited CLSID in INF:\n{inf}"
);
assert!(inf.contains("test_apo.dll"));
assert!(inf.contains("%11%\\test_apo.dll"));
}
#[test]
fn generated_inf_uses_default_threading_model_both() {
let inf = generate(&sample_config());
assert!(
inf.contains("ThreadingModel,0,\"Both\""),
"expected ThreadingModel = Both in InprocServer32"
);
}
#[test]
fn generated_inf_records_driver_version_string() {
let inf = generate(&sample_config());
assert!(
inf.contains("DriverVer = 01/01/2026,1.0.0.0"),
"expected DriverVer with major.minor.0.0 in INF:\n{inf}"
);
}
#[test]
fn clsid_no_braces_strips_outer_braces() {
let c = Clsid::from_u128(0xAABBCCDD_EEFF_0011_2233_445566778899);
assert_eq!(clsid_no_braces(&c), "AABBCCDD-EEFF-0011-2233-445566778899");
}
#[test]
fn clsid_id_string_is_continuous_hex() {
let c = Clsid::from_u128(0x01234567_89AB_CDEF_0123_456789ABCDEF);
assert_eq!(clsid_id_string(&c), "0123456789ABCDEF0123456789ABCDEF");
}
#[test]
fn escape_inf_doubles_internal_quotes() {
assert_eq!(escape_inf(r#"He said "hi""#), r#"He said ""hi"""#);
assert_eq!(escape_inf("nothing to escape"), "nothing to escape");
}
}