extern crate alloc;
use alloc::string::String;
use core::fmt::Write as _;
pub const AUDIO_SERVER_PLUGIN_TYPE_UUID: &str = "443ABAB8-E7B3-491A-B985-BEB9187030DB";
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct BundleConfig {
pub bundle_identifier: &'static str,
pub bundle_name: &'static str,
pub executable: &'static str,
pub version: &'static str,
pub factory_uuid: &'static str,
pub factory_function: &'static str,
}
impl BundleConfig {
#[must_use]
pub const fn new(
bundle_identifier: &'static str,
factory_uuid: &'static str,
factory_function: &'static str,
) -> Self {
Self {
bundle_identifier,
bundle_name: bundle_identifier,
executable: bundle_identifier,
version: "0.0.0",
factory_uuid,
factory_function,
}
}
#[must_use]
pub const fn with_bundle_name(mut self, name: &'static str) -> Self {
self.bundle_name = name;
self
}
#[must_use]
pub const fn with_executable(mut self, executable: &'static str) -> Self {
self.executable = executable;
self
}
#[must_use]
pub const fn with_version(mut self, version: &'static str) -> Self {
self.version = version;
self
}
}
#[must_use]
pub fn generate(config: &BundleConfig) -> String {
let mut out = String::with_capacity(1024);
out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
out.push_str(
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \
\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n",
);
out.push_str("<plist version=\"1.0\">\n");
out.push_str("<dict>\n");
key_string(&mut out, "CFBundleInfoDictionaryVersion", "6.0");
key_string(&mut out, "CFBundleIdentifier", config.bundle_identifier);
key_string(&mut out, "CFBundleName", config.bundle_name);
key_string(&mut out, "CFBundleExecutable", config.executable);
key_string(&mut out, "CFBundleVersion", config.version);
key_string(&mut out, "CFBundleShortVersionString", config.version);
key_string(&mut out, "CFBundlePackageType", "BNDL");
key_string(&mut out, "CFPlugInDynamicRegistration", "NO");
indent(&mut out, 1);
out.push_str("<key>CFPlugInFactories</key>\n");
indent(&mut out, 1);
out.push_str("<dict>\n");
indent(&mut out, 2);
let _ = writeln!(out, "<key>{}</key>", escape_xml(config.factory_uuid));
indent(&mut out, 2);
let _ = writeln!(
out,
"<string>{}</string>",
escape_xml(config.factory_function)
);
indent(&mut out, 1);
out.push_str("</dict>\n");
indent(&mut out, 1);
out.push_str("<key>CFPlugInTypes</key>\n");
indent(&mut out, 1);
out.push_str("<dict>\n");
indent(&mut out, 2);
let _ = writeln!(
out,
"<key>{}</key>",
escape_xml(AUDIO_SERVER_PLUGIN_TYPE_UUID)
);
indent(&mut out, 2);
out.push_str("<array>\n");
indent(&mut out, 3);
let _ = writeln!(out, "<string>{}</string>", escape_xml(config.factory_uuid));
indent(&mut out, 2);
out.push_str("</array>\n");
indent(&mut out, 1);
out.push_str("</dict>\n");
out.push_str("</dict>\n");
out.push_str("</plist>\n");
out
}
fn key_string(out: &mut String, key: &str, value: &str) {
indent(out, 1);
let _ = writeln!(out, "<key>{}</key>", escape_xml(key));
indent(out, 1);
let _ = writeln!(out, "<string>{}</string>", escape_xml(value));
}
fn indent(out: &mut String, level: usize) {
for _ in 0..level {
out.push('\t');
}
}
fn escape_xml(s: &str) -> String {
let mut escaped = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'&' => escaped.push_str("&"),
'<' => escaped.push_str("<"),
'>' => escaped.push_str(">"),
'"' => escaped.push_str("""),
'\'' => escaped.push_str("'"),
other => escaped.push(other),
}
}
escaped
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_config() -> BundleConfig {
BundleConfig::new(
"com.example.MinimalLoopback",
"9E5B7C2A-1D3F-4A6B-8C9D-0E1F2A3B4C5D",
"TympanAsplDriverFactory",
)
.with_bundle_name("Minimal Loopback")
.with_executable("MinimalLoopback")
.with_version("0.1.0")
}
#[test]
fn new_defaults_optional_fields_to_the_identifier() {
let c = BundleConfig::new("com.example.Foo", "UUID", "Factory");
assert_eq!(c.bundle_name, "com.example.Foo");
assert_eq!(c.executable, "com.example.Foo");
assert_eq!(c.version, "0.0.0");
}
#[test]
fn builder_setters_override_defaults() {
let c = sample_config();
assert_eq!(c.bundle_name, "Minimal Loopback");
assert_eq!(c.executable, "MinimalLoopback");
assert_eq!(c.version, "0.1.0");
}
#[test]
fn generated_plist_has_xml_and_doctype_preamble() {
let plist = generate(&sample_config());
assert!(plist.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"));
assert!(plist.contains("<!DOCTYPE plist PUBLIC"));
assert!(plist.contains("<plist version=\"1.0\">"));
assert!(plist.trim_end().ends_with("</plist>"));
}
#[test]
fn generated_plist_embeds_all_cfbundle_keys() {
let plist = generate(&sample_config());
for key in [
"CFBundleInfoDictionaryVersion",
"CFBundleIdentifier",
"CFBundleName",
"CFBundleExecutable",
"CFBundleVersion",
"CFBundleShortVersionString",
"CFBundlePackageType",
"CFPlugInDynamicRegistration",
"CFPlugInFactories",
"CFPlugInTypes",
] {
assert!(
plist.contains(&format!("<key>{key}</key>")),
"missing key {key} in:\n{plist}"
);
}
}
#[test]
fn generated_plist_embeds_identity_and_factory_values() {
let plist = generate(&sample_config());
assert!(plist.contains("<string>com.example.MinimalLoopback</string>"));
assert!(plist.contains("<string>Minimal Loopback</string>"));
assert!(plist.contains("<string>MinimalLoopback</string>"));
assert!(plist.contains("<string>0.1.0</string>"));
assert!(plist.contains("<string>BNDL</string>"));
assert!(plist.contains("<string>TympanAsplDriverFactory</string>"));
assert_eq!(
plist
.matches("9E5B7C2A-1D3F-4A6B-8C9D-0E1F2A3B4C5D")
.count(),
2
);
}
#[test]
fn generated_plist_wires_the_audioserver_type_uuid() {
let plist = generate(&sample_config());
assert!(plist.contains(&format!("<key>{AUDIO_SERVER_PLUGIN_TYPE_UUID}</key>")));
assert!(plist.contains("<array>"));
assert!(plist.contains("</array>"));
}
#[test]
fn audioserver_type_uuid_matches_core_audio() {
assert_eq!(
AUDIO_SERVER_PLUGIN_TYPE_UUID,
"443ABAB8-E7B3-491A-B985-BEB9187030DB"
);
}
#[test]
fn string_values_are_xml_escaped() {
let config = BundleConfig::new("com.example.A&B", "UUID", "Factory<dangerous>")
.with_bundle_name("Quote\"Test");
let plist = generate(&config);
assert!(plist.contains("com.example.A&B"));
assert!(plist.contains("Factory<dangerous>"));
assert!(plist.contains("Quote"Test"));
assert!(!plist.contains("A&B"));
assert!(!plist.contains("<dangerous>"));
}
#[test]
fn escape_xml_handles_all_five_entities() {
assert_eq!(
escape_xml("a&b<c>d\"e'f"),
"a&b<c>d"e'f"
);
assert_eq!(escape_xml("nothing to escape"), "nothing to escape");
}
#[test]
fn key_value_pairs_are_balanced() {
let plist = generate(&sample_config());
assert_eq!(
plist.matches("<dict>").count(),
plist.matches("</dict>").count()
);
assert_eq!(
plist.matches("<array>").count(),
plist.matches("</array>").count()
);
}
}