oth_rvault 0.4.0

Partial Ansible Vault encoder and decoder
Documentation
use crate::types::{TypedValue, ValueType};

const VAULT_PREFIX: &str = "$ANSIBLE_VAULT;";

/// Returns an error if any key is a dot-notation ancestor of another key in the list.
/// E.g. `["second", "second.a"]` conflicts because `"second"` is a prefix of `"second.a"`.
pub fn check_key_conflicts(keys: &[String]) -> Result<(), String> {
    for i in 0..keys.len() {
        for j in 0..keys.len() {
            if i == j {
                continue;
            }
            if keys[j].starts_with(&format!("{}.", keys[i])) {
                return Err(format!(
                    "conflicting keys: '{}' overlaps with '{}'",
                    keys[i], keys[j]
                ));
            }
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_no_conflicts() {
        let keys = vec!["first".to_string(), "second".to_string(), "third.carrot".to_string()];
        assert!(check_key_conflicts(&keys).is_ok());
    }

    #[test]
    fn test_direct_prefix_conflict() {
        let keys = vec!["second".to_string(), "second.a".to_string()];
        assert!(check_key_conflicts(&keys).is_err());
    }

    #[test]
    fn test_deep_prefix_conflict() {
        let keys = vec!["second.a".to_string(), "second.a.v".to_string()];
        assert!(check_key_conflicts(&keys).is_err());
    }

    #[test]
    fn test_non_prefix_similar_names() {
        // "second" is NOT a prefix of "secondly" — dot separator required
        let keys = vec!["second".to_string(), "secondly".to_string()];
        assert!(check_key_conflicts(&keys).is_ok());
    }

    #[test]
    fn test_empty_keys() {
        assert!(check_key_conflicts(&[]).is_ok());
    }

    #[test]
    fn test_single_key() {
        assert!(check_key_conflicts(&["first".to_string()]).is_ok());
    }
}

/// Check if a value is already Ansible Vault encrypted.
/// Returns (is_encrypted, vault_string_from_prefix).
pub fn is_encrypted(value: &TypedValue) -> (bool, Option<String>) {
    let TypedValue::String(s) = value else {
        return (false, None);
    };
    match s.find(VAULT_PREFIX) {
        Some(idx) => (true, Some(s[idx..].to_string())),
        None => (false, None),
    }
}

/// Returns true if the value is of a type that can be encrypted (not Object, Array, or Null).
pub fn is_encryptable(vt: ValueType) -> bool {
    matches!(
        vt,
        ValueType::String | ValueType::Integer | ValueType::Bool | ValueType::Number
    )
}