tellaro-query-language 3.0.1

A flexible, human-friendly query language for searching and filtering structured data
Documentation
//! The TQL string-literal escaping standard.
//!
//! One definition, mirrored byte-for-byte by the Python parser
//! (`src/tql/parser_components/string_escapes.py`) and the JS tokenizer
//! (`js/src/tokenizer.ts`). Conformance across the three is pinned by
//! `cross_language_tests/fixtures/test_cases/syntax/string_literals.json`.
//!
//! # The standard
//!
//! Inside a quoted literal, a backslash introduces an escape sequence:
//!
//! | sequence | meaning                                 |
//! |----------|-----------------------------------------|
//! | `\\`     | a single backslash                      |
//! | `\'`     | a single quote                          |
//! | `\"`     | a double quote                          |
//! | `\n`     | line feed                               |
//! | `\r`     | carriage return                         |
//! | `\t`     | tab                                     |
//! | `\X`     | **literal backslash + X** for any other X |
//!
//! Unknown escapes keep their backslash because Windows paths dominate this
//! product's string values; dropping it silently turns `'\Windows'` into
//! `'Windows'`, producing a query that looks correct and matches nothing.
//!
//! # Why a single pass
//!
//! This previously chained `.replace()` calls, which is order-dependent and
//! wrong: `\\n` (an escaped backslash followed by `n`) was rewritten by the
//! `\n` -> line-feed rule before the `\\` -> `\` rule could claim it. A single
//! left-to-right scan consumes each escape exactly once.

/// Decode the inner text of a quoted TQL literal.
///
/// `raw` is the content BETWEEN the quotes, with the quotes already removed.
/// A trailing lone backslash is preserved literally rather than erroring — the
/// grammar has already established the literal is terminated, so the only way
/// to reach here with one is a value that genuinely ends in a backslash (a
/// directory path, e.g. `C:\Windows\Temp\`).
pub fn unescape_string_literal(raw: &str) -> String {
    if !raw.contains('\\') {
        return raw.to_string();
    }

    let mut out = String::with_capacity(raw.len());
    let mut chars = raw.chars();
    while let Some(c) = chars.next() {
        if c != '\\' {
            out.push(c);
            continue;
        }
        match chars.next() {
            None => out.push('\\'), // trailing lone backslash
            Some('\\') => out.push('\\'),
            Some('\'') => out.push('\''),
            Some('"') => out.push('"'),
            Some('n') => out.push('\n'),
            Some('r') => out.push('\r'),
            Some('t') => out.push('\t'),
            Some(other) => {
                out.push('\\');
                out.push(other);
            }
        }
    }
    out
}

/// Encode a raw string as the inner text of a TQL literal.
///
/// The exact inverse of [`unescape_string_literal`]. Only the active `quote`
/// character is escaped, so a Windows path in single quotes stays readable.
pub fn escape_string_literal(value: &str, quote: char) -> String {
    let mut out = String::with_capacity(value.len());
    for c in value.chars() {
        match c {
            '\\' => out.push_str("\\\\"),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            c if c == quote => {
                out.push('\\');
                out.push(c);
            }
            c => out.push(c),
        }
    }
    out
}

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

    #[test]
    fn defined_escapes() {
        assert_eq!(unescape_string_literal(r"\\"), "\\");
        assert_eq!(unescape_string_literal(r"\'"), "'");
        assert_eq!(unescape_string_literal(r#"\""#), "\"");
        assert_eq!(unescape_string_literal(r"\n"), "\n");
        assert_eq!(unescape_string_literal(r"\r"), "\r");
        assert_eq!(unescape_string_literal(r"\t"), "\t");
    }

    #[test]
    fn unknown_escape_keeps_its_backslash() {
        assert_eq!(unescape_string_literal(r"\W"), r"\W");
        assert_eq!(unescape_string_literal(r"C:\Windows"), r"C:\Windows");
        assert_eq!(unescape_string_literal(r"\d{3}\s\w+"), r"\d{3}\s\w+");
    }

    /// REGRESSION: the old chained-`replace` implementation turned `\\n` into
    /// a backslash followed by a line feed, because the `\n` rule fired first.
    #[test]
    fn escaped_backslash_before_n_is_not_a_newline() {
        assert_eq!(unescape_string_literal(r"\\n"), r"\n");
        assert!(!unescape_string_literal(r"\\n").contains('\n'));
    }

    #[test]
    fn escaped_backslash_before_quote_is_not_an_escaped_quote() {
        assert_eq!(unescape_string_literal(r"\\'"), r"\'");
    }

    #[test]
    fn trailing_lone_backslash_is_preserved() {
        assert_eq!(unescape_string_literal(r"C:\Temp\"), r"C:\Temp\");
    }

    #[test]
    fn windows_path_with_doubled_backslashes() {
        assert_eq!(
            unescape_string_literal(r"C:\\Windows\\ntdll.dll"),
            r"C:\Windows\ntdll.dll"
        );
        assert!(!unescape_string_literal(r"C:\\Windows\\ntdll.dll").contains('\n'));
    }

    #[test]
    fn no_backslash_is_a_fast_path_and_identity() {
        assert_eq!(unescape_string_literal("plain value"), "plain value");
        assert_eq!(unescape_string_literal(""), "");
    }

    #[test]
    fn escape_then_unescape_round_trips() {
        let nasty = [
            r"C:\Windows\ntdll.dll",
            r"C:\Temp\",
            "\\",
            "a\tb",
            "line\nbreak",
            "don't",
            "say \"hi\"",
            "both'and\"quotes",
            "plain",
            r"\W\q",
            "Администратор",
        ];
        for value in nasty {
            assert_eq!(
                unescape_string_literal(&escape_string_literal(value, '\'')),
                value,
                "single-quoted round trip failed for {value:?}"
            );
            assert_eq!(
                unescape_string_literal(&escape_string_literal(value, '"')),
                value,
                "double-quoted round trip failed for {value:?}"
            );
        }
    }

    #[test]
    fn escape_leaves_the_inactive_quote_alone() {
        assert_eq!(escape_string_literal("a\"b", '\''), "a\"b");
        assert_eq!(escape_string_literal("don't", '"'), "don't");
        assert_eq!(escape_string_literal("don't", '\''), r"don\'t");
    }

    #[test]
    fn multibyte_values_are_not_split() {
        assert_eq!(unescape_string_literal("Администратор"), "Администратор");
        assert_eq!(unescape_string_literal("\u{202e}"), "\u{202e}");
    }
}