oth_rvault 0.2.0

Partial Ansible Vault encoder and decoder
Documentation
use serde_yaml::Value;

use crate::types::{new_key_path, ProcessHandling, TypedValue, ValueType};

fn typed_value_from_yaml(val: &Value) -> Option<(TypedValue, ValueType)> {
    match val {
        Value::Bool(b) => Some((TypedValue::Bool(*b), ValueType::Bool)),
        Value::String(s) => Some((TypedValue::String(s.clone()), ValueType::String)),
        Value::Number(n) => {
            if let Some(i) = n.as_i64() {
                // serde_yaml reports is_i64() for numbers parsed from integer literals
                if n.is_i64() {
                    return Some((TypedValue::Integer(i), ValueType::Integer));
                }
            }
            n.as_f64()
                .map(|f| (TypedValue::Number(f), ValueType::Number))
        }
        Value::Null => Some((TypedValue::Null, ValueType::Null)),
        // Ansible's `!vault` tagged values and other custom tags – extract the inner string.
        Value::Tagged(tagged) => match &tagged.value {
            Value::String(s) => Some((TypedValue::String(s.clone()), ValueType::String)),
            inner => typed_value_from_yaml(inner),
        },
        _ => None, // Mapping/Sequence handled by recursion
    }
}

fn typed_value_to_yaml(typed: TypedValue) -> Value {
    match typed {
        TypedValue::Bool(b) => Value::Bool(b),
        TypedValue::Integer(i) => Value::Number(serde_yaml::Number::from(i)),
        TypedValue::Number(f) => Value::Number(serde_yaml::Number::from(f)),
        TypedValue::String(s) => Value::String(s),
        TypedValue::Null => Value::Null,
    }
}

type Processor<'a> = &'a mut dyn FnMut(
    TypedValue,
    ValueType,
    &str,
) -> Result<(TypedValue, ValueType, ProcessHandling), Box<dyn std::error::Error>>;

fn traverse(
    val: &mut Value,
    key_path: &str,
    processor: Processor<'_>,
    filter_keys: &[String],
) -> Result<bool, Box<dyn std::error::Error>> {
    match val {
        Value::Mapping(map) => {
            // Collect keys first so we can get_mut on each without borrow conflicts.
            let keys: Vec<Value> = map.keys().cloned().collect();
            for yaml_key in &keys {
                let key_str = match yaml_key {
                    Value::String(s) => s.clone(),
                    Value::Number(n) => n.to_string(),
                    Value::Bool(b) => b.to_string(),
                    _ => continue, // null/mapping/sequence keys are not supported
                };
                let new_path = new_key_path(key_path, &key_str);
                if let Some(child) = map.get_mut(yaml_key) {
                    if !traverse(child, &new_path, processor, filter_keys)? {
                        return Ok(false);
                    }
                }
            }
        }
        Value::Sequence(seq) => {
            for item in seq.iter_mut() {
                if !traverse(item, key_path, processor, filter_keys)? {
                    return Ok(false);
                }
            }
        }
        scalar => {
            if !filter_keys.is_empty()
                && !filter_keys.iter().any(|k| k.as_str() == key_path)
            {
                return Ok(true);
            }
            if let Some((typed, vt)) = typed_value_from_yaml(scalar) {
                match processor(typed, vt, key_path) {
                    Ok((new_typed, _, ProcessHandling::Process)) => {
                        *scalar = typed_value_to_yaml(new_typed);
                    }
                    Ok((_, _, ProcessHandling::Cancel)) => return Ok(false),
                    Ok((_, _, ProcessHandling::Skip)) => {}
                    Err(e) => {
                        eprintln!("error processing key '{}': {}", key_path, e);
                        return Ok(false);
                    }
                }
            }
        }
    }
    Ok(true)
}

/// Read a YAML file, apply `processor` to every matching scalar value, and write the result.
/// `filter_keys` is a list of dot-notation key paths to restrict processing to; empty means all values.
/// Vault-encrypted strings (with embedded newlines) are written as YAML literal block scalars.
pub fn process_file(
    input: &str,
    output: &str,
    filter_keys: &[String],
    processor: Processor<'_>,
) -> Result<(), Box<dyn std::error::Error>> {
    let content = std::fs::read_to_string(input)
        .map_err(|e| format!("error reading '{}': {}", input, e))?;
    let mut root: Value = serde_yaml::from_str(&content)
        .map_err(|e| format!("error parsing YAML from '{}': {}", input, e))?;

    if !traverse(&mut root, "", processor, filter_keys)? {
        return Err("processing canceled".into());
    }

    let yaml_str = serde_yaml::to_string(&root)?;
    if output == "stdout" {
        print!("{}", yaml_str);
    } else {
        std::fs::write(output, &yaml_str)
            .map_err(|e| format!("error writing '{}': {}", output, e))?;
    }
    Ok(())
}