use super::key_file_data::*;
use std::collections::HashMap;
const SEC_CONFIG: &str = "@KEY_CONFIG";
const SEC_PIPELINE: &str = "@KEY_PIPELINE";
const SEC_ENCRYPTION: &str = "@KEY_ENCRYPTION";
const SEC_COMPRESSION: &str = "@KEY_COMPRESSION";
const SEC_FILE_INFO: &str = "@KEY_FILE_INFO";
pub struct MdixKeyWriter;
impl MdixKeyWriter {
pub fn write(data: &KeyFileData) -> String {
let mut out = String::with_capacity(1400);
out.push_str("// DixScript Key File v1.0.0\n");
out.push_str(&format!(
"// Generated: {}\n",
data.config.generated.format("%Y-%m-%dT%H:%M:%SZ"),
));
if let Some(ref src) = data.config.source_file {
out.push_str(&format!("// Source: {}\n", src));
}
out.push('\n');
out.push_str(SEC_CONFIG);
out.push_str("(\n");
Self::str_entry(&mut out, "version", &data.config.version);
Self::str_entry(&mut out, "key_type", &data.config.key_type);
Self::str_entry(&mut out, "generated", &data.config.generated.format("%Y-%m-%dT%H:%M:%SZ").to_string());
if let Some(ref src) = data.config.source_file {
Self::str_entry(&mut out, "source_file", src);
}
out.push_str(")\n\n");
out.push_str(SEC_PIPELINE);
out.push_str("(\n");
Self::str_entry(&mut out, "modules", &data.pipeline.modules_used.join(","));
Self::str_entry(&mut out, "reversal", &data.pipeline.reversal_order.join(","));
out.push_str(")\n\n");
if let Some(ref enc) = data.key_data.encryption {
out.push_str(SEC_ENCRYPTION);
out.push_str("(\n");
Self::str_entry(&mut out, "algorithm", &enc.algorithm);
Self::uint_entry(&mut out, "key_length", enc.key_length);
Self::str_entry(&mut out, "iv", &enc.iv);
Self::str_entry(&mut out, "security_level", &enc.security_level);
if let Some(ref key_data) = enc.key_data {
Self::str_entry(&mut out, "key_data", key_data);
}
if let Some(ref kdf) = enc.kdf {
Self::str_entry(&mut out, "kdf_algorithm", &kdf.algorithm);
Self::str_entry(&mut out, "kdf_version", &kdf.kdf_version);
Self::u32_entry(&mut out, "kdf_memory", kdf.memory);
Self::u32_entry(&mut out, "kdf_iterations", kdf.iterations);
Self::u32_entry(&mut out, "kdf_parallelism",kdf.parallelism);
Self::str_entry(&mut out, "salt", &kdf.salt);
Self::uint_entry(&mut out, "salt_length", kdf.salt_length);
}
out.push_str(")\n\n");
}
if let Some(ref comp) = data.key_data.compression {
out.push_str(SEC_COMPRESSION);
out.push_str("(\n");
Self::str_entry(&mut out, "algorithm", &comp.algorithm);
Self::uint_entry(&mut out, "original_size", comp.original_size);
Self::uint_entry(&mut out, "compressed_size", comp.compressed_size);
if let Some(ref level) = comp.compression_level {
Self::str_entry(&mut out, "compression_level", level);
}
out.push_str(")\n\n");
}
out.push_str(SEC_FILE_INFO);
out.push_str("(\n");
Self::uint_entry(&mut out, "original_size", data.file_info.original_size);
Self::uint_entry(&mut out, "compressed_size", data.file_info.compressed_size);
Self::uint_entry(&mut out, "encrypted_size", data.file_info.encrypted_size);
Self::str_entry(&mut out, "created",
&data.file_info.created.format("%Y-%m-%dT%H:%M:%SZ").to_string());
if let Some(ref src) = data.file_info.source_file {
Self::str_entry(&mut out, "source_file", src);
}
if let Some(ref out_file) = data.file_info.output_file {
Self::str_entry(&mut out, "output_file", out_file);
}
out.push_str(")\n");
out
}
#[inline]
fn str_entry(out: &mut String, key: &str, value: &str) {
out.push_str(" ");
out.push_str(key);
out.push_str(" -> \"");
out.push_str(value);
out.push_str("\",\n");
}
#[inline]
fn uint_entry(out: &mut String, key: &str, value: usize) {
out.push_str(" ");
out.push_str(key);
out.push_str(" -> ");
let buf = value.to_string();
out.push_str(&buf);
out.push_str(",\n");
}
#[inline]
fn u32_entry(out: &mut String, key: &str, value: u32) {
Self::uint_entry(out, key, value as usize);
}
}
pub struct MdixKeyParser;
impl MdixKeyParser {
pub fn parse(input: &str) -> Result<KeyFileData, String> {
let mut data = KeyFileData::new();
let config_content = Self::extract_section(input, SEC_CONFIG)
.ok_or("Missing @KEY_CONFIG section in key file")?;
let config_entries = Self::parse_entries(&config_content);
data.config.version = Self::req_string(&config_entries, "version", SEC_CONFIG)?;
data.config.key_type = Self::req_string(&config_entries, "key_type", SEC_CONFIG)?;
if let Some(gen) = Self::opt_string(&config_entries, "generated") {
if let Ok(dt) = gen.parse::<chrono::DateTime<chrono::Utc>>() {
data.config.generated = dt;
}
}
data.config.source_file = Self::opt_string(&config_entries, "source_file");
let pipeline_content = Self::extract_section(input, SEC_PIPELINE)
.ok_or("Missing @KEY_PIPELINE section in key file")?;
let pipeline_entries = Self::parse_entries(&pipeline_content);
if let Some(modules) = Self::opt_string(&pipeline_entries, "modules") {
data.pipeline.modules_used = Self::split_csv(&modules);
}
if let Some(reversal) = Self::opt_string(&pipeline_entries, "reversal") {
data.pipeline.reversal_order = Self::split_csv(&reversal);
}
if let Some(enc_content) = Self::extract_section(input, SEC_ENCRYPTION) {
let enc_entries = Self::parse_entries(&enc_content);
let algorithm = Self::req_string(&enc_entries, "algorithm", SEC_ENCRYPTION)?;
let iv = Self::opt_string(&enc_entries, "iv").unwrap_or_default();
let security_level = Self::opt_string(&enc_entries, "security_level").unwrap_or_else(|| "HIGH".to_string());
let key_length = Self::opt_usize(&enc_entries, "key_length").unwrap_or(32);
let kdf = if enc_entries.contains_key("kdf_algorithm") {
Some(KDFParameters {
algorithm: Self::opt_string(&enc_entries, "kdf_algorithm").unwrap_or_else(|| "argon2id".to_string()),
kdf_version: Self::opt_string(&enc_entries, "kdf_version").unwrap_or_else(|| "1.3".to_string()),
memory: Self::opt_u32(&enc_entries, "kdf_memory").unwrap_or(65536),
iterations: Self::opt_u32(&enc_entries, "kdf_iterations").unwrap_or(3),
parallelism: Self::opt_u32(&enc_entries, "kdf_parallelism").unwrap_or(4),
salt: Self::opt_string(&enc_entries, "salt").unwrap_or_default(),
salt_length: Self::opt_usize(&enc_entries, "salt_length").unwrap_or(32),
})
} else {
None
};
data.key_data.encryption = Some(EncryptionKeyData {
algorithm,
key_length,
security_level,
key_data: Self::opt_string(&enc_entries, "key_data"),
iv,
kdf,
});
}
if let Some(comp_content) = Self::extract_section(input, SEC_COMPRESSION) {
let comp_entries = Self::parse_entries(&comp_content);
data.key_data.compression = Some(CompressionKeyData {
algorithm: Self::req_string(&comp_entries, "algorithm", SEC_COMPRESSION)?,
compression_level: Self::opt_string(&comp_entries, "compression_level"),
original_size: Self::opt_usize(&comp_entries, "original_size").unwrap_or(0),
compressed_size: Self::opt_usize(&comp_entries, "compressed_size").unwrap_or(0),
});
}
let info_content = Self::extract_section(input, SEC_FILE_INFO)
.ok_or("Missing @KEY_FILE_INFO section in key file")?;
let info_entries = Self::parse_entries(&info_content);
data.file_info.original_size = Self::opt_usize(&info_entries, "original_size").unwrap_or(0);
data.file_info.compressed_size = Self::opt_usize(&info_entries, "compressed_size").unwrap_or(0);
data.file_info.encrypted_size = Self::opt_usize(&info_entries, "encrypted_size").unwrap_or(0);
data.file_info.source_file = Self::opt_string(&info_entries, "source_file");
data.file_info.output_file = Self::opt_string(&info_entries, "output_file");
if let Some(created_str) = Self::opt_string(&info_entries, "created") {
if let Ok(dt) = created_str.parse::<chrono::DateTime<chrono::Utc>>() {
data.file_info.created = dt;
}
}
Ok(data)
}
fn extract_section(input: &str, section_keyword: &str) -> Option<String> {
let start = input.find(section_keyword)?;
let after_keyword = &input[start + section_keyword.len()..];
let open_offset = after_keyword.find('(')?;
let body = &after_keyword[open_offset..];
let mut depth: i32 = 0;
let mut in_string = false;
let mut string_char = '\0';
let mut close_byte_pos = None;
for (byte_pos, ch) in body.char_indices() {
if in_string {
if ch == string_char {
in_string = false;
}
continue;
}
match ch {
'"' | '\'' => { in_string = true; string_char = ch; }
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
close_byte_pos = Some(byte_pos);
break;
}
}
_ => {}
}
}
let close = close_byte_pos?;
Some(body[1..close].to_string())
}
fn parse_entries(content: &str) -> HashMap<String, String> {
let line_count = content.lines().count();
let mut map = HashMap::with_capacity(line_count.max(4));
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with("//") {
continue;
}
let Some(arrow_pos) = line.find(" -> ") else { continue; };
let key = line[..arrow_pos].trim();
if key.is_empty() {
continue;
}
let raw_value = line[arrow_pos + 4..].trim();
let raw_value = raw_value.strip_suffix(',').unwrap_or(raw_value).trim_end();
let value = if raw_value.len() >= 2
&& raw_value.starts_with('"')
&& raw_value.ends_with('"')
{
&raw_value[1..raw_value.len() - 1]
} else {
raw_value
};
map.insert(key.to_string(), value.to_string());
}
map
}
fn req_string(
map: &HashMap<String, String>,
key: &str,
section: &str,
) -> Result<String, String> {
map.get(key).cloned()
.ok_or_else(|| format!("Missing required field '{}' in {}", key, section))
}
#[inline]
fn opt_string(map: &HashMap<String, String>, key: &str) -> Option<String> {
map.get(key).cloned()
}
#[inline]
fn opt_usize(map: &HashMap<String, String>, key: &str) -> Option<usize> {
map.get(key)?.parse().ok()
}
#[inline]
fn opt_u32(map: &HashMap<String, String>, key: &str) -> Option<u32> {
map.get(key)?.parse().ok()
}
#[inline]
fn split_csv(s: &str) -> Vec<String> {
if s.is_empty() {
return Vec::new();
}
s.split(',').map(|p| p.trim().to_string()).collect()
}
}