winereg 0.1.0

Rust library for parsing, writing, diffing, patching, and scripting Wine/Windows registry files.
Documentation
use crate::registry_comparator::{DiffResult, KeyPropertyChange, RegistryChange};
use crate::registry_value::{RegistryValue, RegistryValueData, REG_BINARY};

pub struct TextDiffExporter;

impl TextDiffExporter {
    pub fn export(&self, diff: &DiffResult, from_file: Option<&str>, to_file: Option<&str>) -> String {
        let mut out = String::new();
        out.push_str("# Registry Patch File\n");
        out.push_str("# Generated: ");
        out.push_str(&chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string());
        out.push('\n');
        if let (Some(f1), Some(f2)) = (from_file, to_file) {
            out.push_str("# FROM: ");
            out.push_str(f1);
            out.push('\n');
            out.push_str("# TO: ");
            out.push_str(f2);
            out.push('\n');
        }
        out.push('\n');
        if !diff.has_changes() {
            out.push_str("# No changes\n");
            return out;
        }

        let mut grouped: std::collections::BTreeMap<String, Vec<RegistryChange>> = std::collections::BTreeMap::new();
        for change in &diff.changes {
            let key = match change {
                RegistryChange::KeyAdded(p) => parent_path(p),
                RegistryChange::KeyDeleted(p) => parent_path(p),
                RegistryChange::KeyModified(p, _) => p.clone(),
                RegistryChange::ValueAdded(k, _, _) => k.clone(),
                RegistryChange::ValueDeleted(k, _, _) => k.clone(),
                RegistryChange::ValueModified(k, _, _, _) => k.clone(),
            };
            grouped.entry(key).or_default().push(change.clone());
        }

        for (path, changes) in grouped {
            let header = if path.is_empty() {
                "[ROOT]".to_string()
            } else {
                format!("[{}]", path)
            };
            out.push_str(&header);
            out.push('\n');
            for c in changes {
                match c {
                    RegistryChange::KeyAdded(p) => {
                        let name = leaf_name(&p);
                        out.push_str("+key:");
                        out.push_str(&name);
                        out.push('\n');
                    }
                    RegistryChange::KeyDeleted(p) => {
                        let name = leaf_name(&p);
                        out.push_str("-key:");
                        out.push_str(&name);
                        out.push('\n');
                    }
                    RegistryChange::KeyModified(_, props) => {
                        for prop in props {
                            match prop {
                                KeyPropertyChange::ClassNameChange(old, newv) => {
                                    out.push_str("~className:");
                                    out.push_str(&format_property(&old));
                                    out.push_str("->");
                                    out.push_str(&format_property(&newv));
                                    out.push('\n');
                                }
                                KeyPropertyChange::SymlinkChange(old, newv) => {
                                    out.push_str(&format!("~isSymlink:{}->{}\n", old, newv));
                                }
                                KeyPropertyChange::VolatileChange(old, newv) => {
                                    out.push_str(&format!("~isVolatile:{}->{}\n", old, newv));
                                }
                            }
                        }
                    }
                    RegistryChange::ValueAdded(_, name, value) => {
                        out.push('+');
                        out.push_str(&format_value(&name, &value));
                        out.push('\n');
                    }
                    RegistryChange::ValueDeleted(_, name, value) => {
                        out.push('-');
                        out.push_str(&format_value(&name, &value));
                        out.push('\n');
                    }
                    RegistryChange::ValueModified(_, name, old, newv) => {
                        out.push('~');
                        out.push('"');
                        out.push_str(&escape_string(&name));
                        out.push_str("\"=");
                        out.push_str(&format_value_data(&old));
                        out.push_str("->");
                        out.push_str(&format_value_data(&newv));
                        out.push('\n');
                    }
                }
            }
            out.push('\n');
        }

        out
    }
}

pub struct TextDiffParser;

impl TextDiffParser {
    pub fn parse(&self, text: &str) -> Result<DiffResult, String> {
        let mut path = String::new();
        let mut changes = Vec::new();
        let mut key_props: std::collections::BTreeMap<String, Vec<KeyPropertyChange>> = std::collections::BTreeMap::new();

        for (idx, line) in text.lines().enumerate() {
            let trimmed = line.trim();
            if trimmed.is_empty() || trimmed.starts_with('#') {
                continue;
            }
            if trimmed.starts_with('[') && trimmed.ends_with(']') {
                path = trimmed[1..trimmed.len() - 1].to_string();
                if path == "ROOT" {
                    path.clear();
                }
                continue;
            }
            if trimmed.starts_with("+key:") {
                let name = trimmed["+key:".len()..].to_string();
                let full = join_path(&path, &name);
                changes.push(RegistryChange::KeyAdded(full));
                continue;
            }
            if trimmed.starts_with("-key:") {
                let name = trimmed["-key:".len()..].to_string();
                let full = join_path(&path, &name);
                changes.push(RegistryChange::KeyDeleted(full));
                continue;
            }
            if trimmed.starts_with("~className:") {
                let rest = &trimmed["~className:".len()..];
                let (old, newv) = split_arrow(rest)?;
                key_props.entry(path.clone()).or_default().push(KeyPropertyChange::ClassNameChange(parse_property_value(old), parse_property_value(newv)));
                continue;
            }
            if trimmed.starts_with("~isSymlink:") {
                let (old, newv) = split_arrow(&trimmed["~isSymlink:".len()..])?;
                let old_b = old.trim().parse::<bool>().map_err(|_| format!("line {}", idx + 1))?;
                let new_b = newv.trim().parse::<bool>().map_err(|_| format!("line {}", idx + 1))?;
                key_props.entry(path.clone()).or_default().push(KeyPropertyChange::SymlinkChange(old_b, new_b));
                continue;
            }
            if trimmed.starts_with("~isVolatile:") {
                let (old, newv) = split_arrow(&trimmed["~isVolatile:".len()..])?;
                let old_b = old.trim().parse::<bool>().map_err(|_| format!("line {}", idx + 1))?;
                let new_b = newv.trim().parse::<bool>().map_err(|_| format!("line {}", idx + 1))?;
                key_props.entry(path.clone()).or_default().push(KeyPropertyChange::VolatileChange(old_b, new_b));
                continue;
            }
            if trimmed.starts_with("+\"") || trimmed.starts_with("-\"") {
                let add = trimmed.starts_with('+');
                let val_part = &trimmed[1..];
                let (name, value) = parse_value(val_part)?;
                let full_change = if add {
                    RegistryChange::ValueAdded(path.clone(), name, value)
                } else {
                    RegistryChange::ValueDeleted(path.clone(), name, value)
                };
                changes.push(full_change);
                continue;
            }
            if trimmed.starts_with("~\"") {
                let val_part = &trimmed[1..];
                let (name, old_value, new_value) = parse_value_modification(val_part)?;
                changes.push(RegistryChange::ValueModified(path.clone(), name, old_value, new_value));
                continue;
            }
        }

        for (path, props) in key_props {
            changes.push(RegistryChange::KeyModified(path, props));
        }

        Ok(DiffResult { changes })
    }
}

fn parent_path(path: &str) -> String {
    path.rsplit_once('\\').map(|(p, _)| p.to_string()).unwrap_or_else(|| "".into())
}

fn leaf_name(path: &str) -> String {
    path.rsplit_once('\\').map(|(_, n)| n.to_string()).unwrap_or_else(|| path.to_string())
}

fn join_path(base: &str, name: &str) -> String {
    if base.is_empty() {
        name.to_string()
    } else {
        format!("{}\\{}", base, name)
    }
}

fn format_property(v: &Option<String>) -> String {
    match v {
        Some(s) => format!("\"{}\"", escape_string(s)),
        None => "null".into(),
    }
}

fn format_value(name: &str, value: &RegistryValue) -> String {
    let mut s = String::new();
    s.push('"');
    s.push_str(&escape_string(name));
    s.push_str("\"=");
    s.push_str(&format_value_data(value));
    s
}

fn format_value_data(value: &RegistryValue) -> String {
    match &value.data {
        RegistryValueData::String(v) => format!("string:\"{}\"", escape_string(v)),
        RegistryValueData::ExpandString(v) => format!("expand_string:\"{}\"", escape_string(v)),
        RegistryValueData::MultiString(vs) => {
            let joined = vs.iter().map(|s| format!("\"{}\"", escape_string(s))).collect::<Vec<_>>().join(",");
            format!("multi_string:[{}]", joined)
        }
        RegistryValueData::Dword(v) => format!("dword:{:08x}", v),
        RegistryValueData::Qword(v) => format!("qword:{:016x}", v),
        RegistryValueData::Binary(bytes, ty) => {
            let prefix = if *ty == REG_BINARY { "hex:".to_string() } else { format!("hex({:x}):", ty) };
            let body = bytes.iter().map(|b| format!("{:02x}", b)).collect::<Vec<_>>().join(",");
            format!("{}{}", prefix, body)
        }
    }
}

fn escape_string(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
}

fn split_arrow(s: &str) -> Result<(&str, &str), String> {
    let pos = s.find("->").ok_or_else(|| "missing ->".to_string())?;
    Ok((&s[..pos], &s[pos + 2..]))
}

fn parse_property_value(text: &str) -> Option<String> {
    let trimmed = text.trim();
    if trimmed == "null" {
        None
    } else {
        Some(trimmed.trim_matches('"').to_string())
    }
}

fn parse_value(line: &str) -> Result<(String, RegistryValue), String> {
    let eq = line.find('=').ok_or("invalid value line")?;
    let raw_name = &line[..eq];
    let name = raw_name.trim_matches('"').to_string();
    let value_part = &line[eq + 1..];
    let value = parse_value_data_part(value_part)?;
    Ok((unescape(&name), value))
}

fn parse_value_modification(line: &str) -> Result<(String, RegistryValue, RegistryValue), String> {
    let eq = line.find('=').ok_or("invalid modification")?;
    let raw_name = &line[..eq];
    let name = unescape(raw_name.trim_matches('"'));
    let rest = &line[eq + 1..];
    let (old_part, new_part) = split_arrow(rest)?;
    let old_val = parse_value_data_part(old_part)?;
    let new_val = parse_value_data_part(new_part)?;
    Ok((name, old_val, new_val))
}

fn parse_value_data_part(data: &str) -> Result<RegistryValue, String> {
    let trimmed = data.trim();
    if trimmed.starts_with("string:") {
        let s = trimmed["string:".len()..].trim().trim_matches('"').to_string();
        return Ok(RegistryValue::new("", RegistryValueData::String(unescape(&s))));
    }
    if trimmed.starts_with("expand_string:") {
        let s = trimmed["expand_string:".len()..].trim().trim_matches('"').to_string();
        return Ok(RegistryValue::new("", RegistryValueData::ExpandString(unescape(&s))));
    }
    if trimmed.starts_with("multi_string:") {
        let content = trimmed["multi_string:".len()..].trim();
        let inner = content.trim_matches(['[', ']'].as_ref());
        let mut values = Vec::new();
        if !inner.is_empty() {
            for part in inner.split(',') {
                let v = part.trim().trim_matches('"');
                values.push(unescape(v));
            }
        }
        return Ok(RegistryValue::new("", RegistryValueData::MultiString(values)));
    }
    if trimmed.starts_with("dword:") {
        let v = u32::from_str_radix(trimmed["dword:".len()..].trim(), 16).map_err(|e| e.to_string())?;
        return Ok(RegistryValue::new("", RegistryValueData::Dword(v)));
    }
    if trimmed.starts_with("qword:") {
        let v = u64::from_str_radix(trimmed["qword:".len()..].trim(), 16).map_err(|e| e.to_string())?;
        return Ok(RegistryValue::new("", RegistryValueData::Qword(v)));
    }
    if trimmed.starts_with("hex(") {
        let end = trimmed.find("):").ok_or("bad hex")?;
        let ty = u32::from_str_radix(&trimmed[4..end], 16).map_err(|e| e.to_string())?;
        let bytes = parse_hex_bytes(&trimmed[end + 2..])?;
        return Ok(RegistryValue::new("", RegistryValueData::Binary(bytes, ty)));
    }
    if trimmed.starts_with("hex:") {
        let bytes = parse_hex_bytes(&trimmed["hex:".len()..])?;
        return Ok(RegistryValue::new("", RegistryValueData::Binary(bytes, REG_BINARY)));
    }
    Err("unknown value format".into())
}

fn parse_hex_bytes(data: &str) -> Result<Vec<u8>, String> {
    let mut bytes = Vec::new();
    if data.trim().is_empty() {
        return Ok(bytes);
    }
    for part in data.split(',') {
        let byte = u8::from_str_radix(part.trim(), 16).map_err(|e| e.to_string())?;
        bytes.push(byte);
    }
    Ok(bytes)
}

fn unescape(s: &str) -> String {
    let mut out = String::new();
    let mut chars = s.chars().peekable();
    while let Some(c) = chars.next() {
        if c == '\\' {
            if let Some(n) = chars.next() {
                match n {
                    '\\' => out.push('\\'),
                    '"' => out.push('"'),
                    'n' => out.push('\n'),
                    'r' => out.push('\r'),
                    't' => out.push('\t'),
                    '0' => out.push('\u{0}'),
                    _ => out.push(n),
                }
            }
        } else {
            out.push(c);
        }
    }
    out
}