use crate::core::types::*;
use crate::tripwire::hasher;
fn push_opt<'a>(components: &mut Vec<&'a str>, field: &'a Option<String>) {
if let Some(ref val) = *field {
components.push(val);
}
}
fn push_list<'a>(components: &mut Vec<&'a str>, items: &'a [String]) {
for item in items {
components.push(item);
}
}
fn collect_core_fields<'a>(components: &mut Vec<&'a str>, resource: &'a Resource) {
push_opt(components, &resource.state);
push_opt(components, &resource.provider);
push_list(components, &resource.packages);
push_opt(components, &resource.path);
push_opt(components, &resource.content);
push_opt(components, &resource.source);
push_opt(components, &resource.name);
push_opt(components, &resource.owner);
push_opt(components, &resource.group);
push_opt(components, &resource.mode);
push_opt(components, &resource.fs_type);
push_opt(components, &resource.options);
push_opt(components, &resource.target);
push_opt(components, &resource.version);
}
fn canonical_overlay_hosts(resource: &Resource) -> String {
let Some(hosts) = resource.overlay_hosts.as_ref() else {
return String::new();
};
if hosts.is_empty() {
return String::new();
}
let mut entries: Vec<(&String, &String)> = hosts.iter().collect();
entries.sort_by(|a, b| a.0.cmp(b.0).then(a.1.cmp(b.1)));
let mut out = String::from("overlay_hosts:");
for (name, ip) in entries {
out.push_str(name);
out.push('=');
out.push_str(ip);
out.push(';');
}
out
}
fn collect_phase2_fields<'a>(
components: &mut Vec<&'a str>,
resource: &'a Resource,
overlay_hosts_canon: &'a str,
) {
push_opt(components, &resource.image);
push_opt(components, &resource.command);
push_opt(components, &resource.schedule);
push_opt(components, &resource.restart);
push_opt(components, &resource.port);
push_opt(components, &resource.protocol);
push_opt(components, &resource.action);
push_opt(components, &resource.from_addr);
push_opt(components, &resource.shell);
push_opt(components, &resource.home);
if let Some(ref enabled) = resource.enabled {
components.push(if *enabled { "enabled" } else { "disabled" });
}
push_list(components, &resource.ports);
push_list(components, &resource.environment);
push_list(components, &resource.volumes);
push_list(components, &resource.restart_on);
push_opt(components, &resource.overlay_ip);
push_opt(components, &resource.overlay_iface);
if let Some(fw) = resource.overlay_firewall {
components.push(if fw {
"overlay_fw_on"
} else {
"overlay_fw_off"
});
}
if !overlay_hosts_canon.is_empty() {
components.push(overlay_hosts_canon);
}
}
pub fn hash_desired_state(resource: &Resource) -> String {
let type_str = resource.resource_type.to_string();
let overlay_hosts_canon = canonical_overlay_hosts(resource);
let mut components: Vec<&str> = vec![&type_str];
collect_core_fields(&mut components, resource);
collect_phase2_fields(&mut components, resource, &overlay_hosts_canon);
let joined = components.join("\0");
let result = hasher::hash_string(&joined);
debug_assert_eq!(
result,
hasher::hash_string(&joined),
"hash_desired_state: determinism violated"
);
result
}