darklua 0.1.0

Obfuscate Lua 5.1 scripts
Documentation
use crate::lua_generator::{LuaGenerator, ToLua};

use std::str::CharIndices;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StringExpression {
    value: String,
}

impl StringExpression {
    pub fn new(string: String) -> Option<Self> {
        let mut chars = string.chars();
        let value = match chars.next() {
            Some('"') | Some('\'') => string.get(1..string.len()-1).map(str::to_owned),
            Some('[') => chars.enumerate()
                    .find_map(|(indice, character)| if character == '[' { Some(indice) } else { None })
                    .and_then(|indice| {
                        let length = 2 + indice;
                        string.get(length..string.len()-length).map(str::to_owned)
                    }),
            _ => None,
        };

        value.map(|value| Self { value })
    }

    pub fn from_value<T: Into<String>>(value: T) -> Self {
        Self { value: value.into() }
    }

    pub fn get_value(&self) -> &str {
        &self.value
    }

    pub fn is_multiline(&self) -> bool {
        self.value.find("\n").is_some()
    }

    pub fn has_single_quote(&self) -> bool {
        self.find_not_escaped('\'').is_some()
    }

    pub fn has_double_quote(&self) -> bool {
        self.find_not_escaped('"').is_some()
    }

    fn find_not_escaped(&self, pattern: char) -> Option<usize> {
        self.find_not_escaped_from(pattern, &mut self.value.char_indices())
    }

    fn find_not_escaped_from(&self, pattern: char, chars: &mut CharIndices) -> Option<usize> {
        let mut escaped = false;
        chars.find_map(|(index, character)| {
            if escaped {
                escaped = false;
                None
            } else {
                match character {
                    '\\' => {
                        escaped = true;
                        None
                    }
                    value => if value == pattern {
                        Some(index)
                    } else {
                        None
                    },
                }
            }
        })
    }
}

impl ToLua for StringExpression {
    fn to_lua(&self, generator: &mut LuaGenerator) {
        let string = if self.is_multiline() {
            let mut i = 0;
            let mut equals = "=".repeat(i);

            loop {
                if self.value.find(&format!("]{}]", equals)).is_none() {
                    break
                } else {
                    i += 1;
                    equals = "=".repeat(i);
                };
            }

            format!("[{}[{}]{}]", equals, self.value, equals)
        } else {
            if self.has_single_quote() {
                if self.has_double_quote() {
                    let mut total_escaped = 0;
                    let mut escaped_string = self.value.clone();

                    let mut chars = self.value.char_indices();

                    while let Some(unescaped_index) = self.find_not_escaped_from('\'', &mut chars) {
                        escaped_string.insert(unescaped_index + total_escaped, '\\');
                        total_escaped += 1;
                    }

                    format!("'{}'", escaped_string)
                } else {
                    format!("\"{}\"", self.value)
                }
            } else {
                format!("'{}'", self.value)
            }
        };

        generator.push_str(&string);
    }
}

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

    #[test]
    fn new_removes_double_quotes() {
        let string = StringExpression::new(r#""hello""#.to_owned()).unwrap();

        assert_eq!(string.get_value(), "hello");
    }

    #[test]
    fn new_removes_single_quotes() {
        let string = StringExpression::new("'hello'".to_owned()).unwrap();

        assert_eq!(string.get_value(), "hello");
    }

    #[test]
    fn new_removes_double_brackets() {
        let string = StringExpression::new("[[hello]]".to_owned()).unwrap();

        assert_eq!(string.get_value(), "hello");
    }

    #[test]
    fn new_removes_double_brackets_with_one_equals() {
        let string = StringExpression::new("[=[hello]=]".to_owned()).unwrap();

        assert_eq!(string.get_value(), "hello");
    }

    #[test]
    fn new_removes_double_brackets_with_multiple_equals() {
        let string = StringExpression::new("[==[hello]==]".to_owned()).unwrap();

        assert_eq!(string.get_value(), "hello");
    }

    #[test]
    fn has_single_quote_is_false_if_no_single_quotes() {
        let string = StringExpression::from_value("hello");

        assert_eq!(string.has_single_quote(), false);
    }

    #[test]
    fn has_single_quote_is_true_if_unescaped_single_quotes() {
        let string = StringExpression::from_value("don't");

        assert_eq!(string.has_single_quote(), true);
    }

    #[test]
    fn has_single_quote_is_true_if_unescaped_single_quotes_with_escaped_backslash() {
        let string = StringExpression::from_value(r#"don\\'t"#);

        assert_eq!(string.has_single_quote(), true);
    }

    #[test]
    fn has_single_quote_is_false_if_escaped_single_quotes() {
        let string = StringExpression::from_value(r#"don\'t"#);

        assert_eq!(string.has_single_quote(), false);
    }

    #[test]
    fn has_double_quote_is_false_if_no_double_quotes() {
        let string = StringExpression::from_value("hello");

        assert_eq!(string.has_double_quote(), false);
    }

    #[test]
    fn has_double_quote_is_true_if_unescaped_double_quotes() {
        let string = StringExpression::from_value(r#"Say: "Hi!""#);

        assert_eq!(string.has_double_quote(), true);
    }

    #[test]
    fn has_double_quote_is_false_if_escaped_double_quotes() {
        let string = StringExpression::from_value(r#"hel\"o"#);

        assert_eq!(string.has_double_quote(), false);
    }

    #[test]
    fn generate_string_without_quotes_uses_single_quotes() {
        let output = StringExpression::from_value("hello").to_lua_string();

        assert_eq!(output, "'hello'");
    }

    #[test]
    fn generate_string_with_single_quotes_uses_double_quotes() {
        let output = StringExpression::from_value("don\'t").to_lua_string();

        assert_eq!(output, r#""don't""#);
    }

    #[test]
    fn generate_string_with_single_and_double_quotes_escapes_single_quotes() {
        let output = StringExpression::from_value(r#"Say: "Don't""#).to_lua_string();

        assert_eq!(output, r#"'Say: "Don\'t"'"#);
    }

    mod snapshot {
        use super::*;

        use insta::assert_snapshot;

        macro_rules! do_snapshots {
            ($($name:ident => $string:literal),+) => {
                $(
                    #[test]
                    fn $name() {
                        assert_snapshot!(
                            stringify!($name),
                            StringExpression::from_value(r#"Say: "Don't""#).to_lua_string()
                        );
                    }
                )+
            };
        }

        do_snapshots!(
            single_quotes => r#"hello"#,
            double_quotes => r#"I'm cool"#,
            escape_single_quotes => r#"Say: "Don't""#
        );
    }
}