use std::path::{Path, PathBuf};
use regex::Regex;
use crate::error::KnxprodError;
use crate::hash::hash_application_program;
use crate::split::SplitResult;
pub fn sign_application(split: &SplitResult) -> Result<SplitResult, KnxprodError> {
let app_path = &split.application;
let xml = std::fs::read_to_string(app_path).map_err(|e| KnxprodError::io(app_path, e))?;
let hash = hash_application_program(&xml)?;
let new_hash_b64 = hash.hash_base64();
let new_fingerprint = hash.fingerprint_hex();
let filename = app_path
.file_name()
.and_then(|f| f.to_str())
.ok_or_else(|| KnxprodError::InvalidStructure("invalid application filename".into()))?;
let old_fingerprint = extract_fingerprint(filename).ok_or_else(|| {
KnxprodError::InvalidStructure(format!(
"cannot extract fingerprint from filename: {filename}"
))
})?;
let patched = patch_hash_attribute(&xml, &new_hash_b64);
let patched = patch_fingerprint(&patched, &old_fingerprint, &new_fingerprint);
let new_filename = filename.replace(&old_fingerprint, &new_fingerprint);
let new_path = app_path.with_file_name(&new_filename);
std::fs::write(&new_path, patched.as_bytes()).map_err(|e| KnxprodError::io(&new_path, e))?;
if new_path != *app_path {
let _ = std::fs::remove_file(app_path);
}
Ok(SplitResult {
catalog: split.catalog.clone(),
hardware: split.hardware.clone(),
application: new_path,
})
}
fn extract_fingerprint(filename: &str) -> Option<String> {
let re = Regex::new(r"_A-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{2}-([0-9A-Fa-f]{4})").ok()?;
re.captures(filename).map(|c| c[1].to_string())
}
#[allow(clippy::expect_used)]
fn patch_hash_attribute(xml: &str, new_hash: &str) -> String {
let re = Regex::new(r#"(<ApplicationProgram[^>]*?)Hash="[^"]*""#).expect("valid regex");
re.replace(xml, format!("${{1}}Hash=\"{new_hash}\""))
.into_owned()
}
#[allow(clippy::expect_used)]
fn patch_fingerprint(xml: &str, old_fp: &str, new_fp: &str) -> String {
let escaped = regex::escape(old_fp);
let pattern = format!(r"(?i)(_A-[0-9A-Fa-f]{{4}}-[0-9A-Fa-f]{{2}}-){escaped}");
let re = Regex::new(&pattern).expect("valid regex");
re.replace_all(xml, format!("${{1}}{new_fp}")).into_owned()
}
#[must_use]
pub fn signed_filename(original: &Path, fingerprint: &str) -> PathBuf {
let filename = original.file_name().and_then(|f| f.to_str()).unwrap_or("");
extract_fingerprint(filename).map_or_else(
|| original.to_path_buf(),
|old_fp| original.with_file_name(filename.replace(&old_fp, fingerprint)),
)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::hash::hash_application_program;
#[test]
fn sign_patches_hash_and_fingerprint() {
let xml = include_str!("../tests/fixtures/leakage_app.xml");
let original_hash = hash_application_program(xml).unwrap();
let dir = tempfile::tempdir().unwrap();
let app_path = dir.path().join("M-0083_A-014F-10-0000.xml");
std::fs::write(&app_path, xml).unwrap();
let split = SplitResult {
catalog: dir.path().join("Catalog.xml"),
hardware: dir.path().join("Hardware.xml"),
application: app_path,
};
let result = sign_application(&split).unwrap();
let new_name = result.application.file_name().unwrap().to_str().unwrap();
assert!(
new_name.contains(&original_hash.fingerprint_hex()),
"filename should contain fingerprint {}, got {new_name}",
original_hash.fingerprint_hex()
);
let patched_xml = std::fs::read_to_string(&result.application).unwrap();
assert!(
patched_xml.contains(&format!("Hash=\"{}\"", original_hash.hash_base64())),
"XML should contain Hash attribute"
);
}
}