snip-it 1.3.1

Fast terminal snippet manager with fuzzy search, TUI, variable expansion, and end-to-end encrypted cross-device sync
Documentation
//! TOML escape sequence handling.
//!
//! Handles edge cases with backslash-containing strings in TOML configuration.
//!
//! # Problem
//!
//! TOML double-quoted strings interpret `\<` as an escape sequence, which fails
//! because `\<` is not a valid TOML escape. This module converts such strings
//! to single-quoted TOML strings which are raw literals.

use regex::Regex;
use std::sync::LazyLock;

static TOML_STRING_PATTERN: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r#""([^"\\]*(?:\\.[^"\\]*)*)""#).expect("Invalid regex"));

/// Internal helper that applies a condition to decide whether to convert
/// double-quoted TOML strings to single-quoted (or escape backslashes).
fn fix_toml_strings(toml_str: &str, needs_fix: impl Fn(&str) -> bool) -> String {
    let mut result = String::with_capacity(toml_str.len());
    let mut last_end = 0;

    for cap in TOML_STRING_PATTERN.captures_iter(toml_str) {
        let full_match = cap.get(0).expect("regex match guarantees group 0");
        let content = cap.get(1).expect("regex pattern captures group 1").as_str();

        result.push_str(&toml_str[last_end..full_match.start()]);

        if needs_fix(content) {
            if content.contains('\'') {
                let escaped = content.replace('\\', "\\\\");
                result.push('"');
                result.push_str(&escaped);
                result.push('"');
            } else {
                // Unescape TOML double-quote escapes so single-quoted output
                // preserves the original literal content. Single-quoted TOML
                // strings are raw, so \\ must become \ and \" must become ".
                let unescaped = content.replace("\\\\", "\\").replace("\\\"", "\"");
                result.push('\'');
                result.push_str(&unescaped);
                result.push('\'');
            }
        } else {
            result.push_str(full_match.as_str());
        }

        last_end = full_match.end();
    }

    result.push_str(&toml_str[last_end..]);
    result
}

/// Converts double-quoted strings in TOML to single-quoted strings if they contain
/// backslashes. This is necessary because TOML double-quoted strings interpret
/// backslash-escape sequences, while single-quoted strings are raw literals.
///
/// For strings containing both backslashes and single quotes, the backslash is
/// escaped instead (using \\) to maintain valid TOML with double quotes.
pub fn quote_strings_containing_backslashes(toml_str: &str) -> String {
    fix_toml_strings(toml_str, |content| content.contains('\\'))
}

/// Fixes invalid TOML escape sequences in double-quoted strings before parsing.
/// Handles `\<` and `\>` which are not valid TOML escape sequences.
///
/// For double-quoted strings containing `\<` or `\>`:
/// - If no single quotes in string: converts the string to single-quoted (preserving content)
/// - If single quotes present: escapes backslash with \\ in double quotes
///
/// Note: This regex only matches single-line strings. Multi-line TOML strings (triple-quoted)
/// are not processed, which is acceptable because snippet commands are single-line.
pub fn fix_invalid_toml_escapes(toml_str: &str) -> String {
    fix_toml_strings(toml_str, |content| {
        content.contains("\\<") || content.contains("\\>")
    })
}

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

    #[test]
    fn test_no_change_when_no_backslash() {
        let input = r#"key = "hello world""#;
        let result = quote_strings_containing_backslashes(input);
        assert_eq!(result, input);
    }

    #[test]
    fn test_backslash_converted_to_single_quotes() {
        let input = "command = \"ping \\<website\\>\"";
        let result = quote_strings_containing_backslashes(input);
        assert_eq!(result, "command = 'ping \\<website\\>'");
    }

    #[test]
    fn test_backslash_and_single_quote_escaped() {
        let input = "command = \"echo 'test\\\\value'\"";
        let result = quote_strings_containing_backslashes(input);
        assert_eq!(result, "command = \"echo 'test\\\\\\\\value'\"");
    }

    #[test]
    fn test_multiple_strings() {
        let input = "command = \"ping \\<website\\>\"\nname = \"test\"";
        let result = quote_strings_containing_backslashes(input);
        assert_eq!(result, "command = 'ping \\<website\\>'\nname = \"test\"");
    }

    #[test]
    fn test_nested_quotes() {
        let input = r#"command = "echo hello""#;
        let result = quote_strings_containing_backslashes(input);
        assert_eq!(result, input);
    }

    #[test]
    fn test_empty_string() {
        let input = r#"key = """#;
        let result = quote_strings_containing_backslashes(input);
        assert_eq!(result, input);
    }

    #[test]
    fn test_escaped_backslash_in_double_quotes() {
        let input = r#"path = "C:\\Users\\test""#;
        let result = quote_strings_containing_backslashes(input);
        assert_eq!(result, r#"path = 'C:\Users\test'"#);
    }

    #[test]
    fn test_fix_invalid_escape_simple() {
        let input = r#"command = "sudo iptables-restore \< /path""#;
        let result = fix_invalid_toml_escapes(input);
        assert_eq!(result, r#"command = 'sudo iptables-restore \< /path'"#);
    }

    #[test]
    fn test_fix_invalid_escape_with_single_quotes() {
        let input = r#"command = "echo 'test \<value'""#;
        let result = fix_invalid_toml_escapes(input);
        assert_eq!(result, r#"command = "echo 'test \\<value'""#);
    }

    #[test]
    fn test_fix_invalid_escape_multiple() {
        let input = r#"command = "echo \<foo\> and \<bar\>""#;
        let result = fix_invalid_toml_escapes(input);
        assert_eq!(result, r#"command = 'echo \<foo\> and \<bar\>'"#);
    }

    #[test]
    fn test_fix_invalid_escape_no_change_needed() {
        let input = r#"command = "echo hello""#;
        let result = fix_invalid_toml_escapes(input);
        assert_eq!(result, input);
    }

    #[test]
    fn test_fix_invalid_escape_with_single_quote_and_invalid_escape() {
        let input = r#"command = "it's a \<test\>""#;
        let result = fix_invalid_toml_escapes(input);
        assert_eq!(result, r#"command = "it's a \\<test\\>""#);
    }

    #[test]
    fn test_fix_invalid_escape_with_escaped_quote() {
        // Input: "it's a \"test\" \<value\>"
        // The regex captures the full content including \" as escaped chars.
        // Since content has single quotes, backslashes are doubled.
        let input = "command = \"it's a \\\"test\\\" \\<value\\>\"";
        let result = fix_invalid_toml_escapes(input);
        // Content: it's a \"test\" \<value\>  →  it's a \\"test\\" \\<value\\>
        assert!(result.contains("\\\\\"test\\\\\""));
        assert!(result.contains("\\\\<value\\\\>"));
    }

    #[test]
    fn test_fix_invalid_escape_in_single_quoted() {
        let input = r#"command = 'echo \<test\>'"#;
        let result = fix_invalid_toml_escapes(input);
        assert_eq!(result, input);
    }

    #[test]
    fn test_multiline_string_ignored() {
        // TOML triple-quoted multiline strings use """...""".
        // The regex processes them as individual double-quoted segments,
        // which may produce unexpected results. This test documents the
        // behavior. Since snippet commands are always single-line, this
        // edge case doesn't affect normal usage.
        let input = "key = \"no escapes here\"";
        let result = fix_invalid_toml_escapes(input);
        assert_eq!(result, input);

        // Strings without invalid escapes are left unchanged
        let input2 = "command = \"echo hello\"";
        let result2 = fix_invalid_toml_escapes(input2);
        assert_eq!(result2, input2);
    }
}