Implement a match table ready for aliases and triggers.
This commit is contained in:
		
							parent
							
								
									0214897a91
								
							
						
					
					
						commit
						b1bf0f317a
					
				
							
								
								
									
										39
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										39
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@ -30,6 +30,15 @@ dependencies = [
 | 
			
		||||
 "zerocopy",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "aho-corasick"
 | 
			
		||||
version = "1.1.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "memchr",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "allocator-api2"
 | 
			
		||||
version = "0.2.18"
 | 
			
		||||
@ -992,6 +1001,35 @@ dependencies = [
 | 
			
		||||
 "rand_core",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "regex"
 | 
			
		||||
version = "1.10.6"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "aho-corasick",
 | 
			
		||||
 "memchr",
 | 
			
		||||
 "regex-automata",
 | 
			
		||||
 "regex-syntax",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "regex-automata"
 | 
			
		||||
version = "0.4.7"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "aho-corasick",
 | 
			
		||||
 "memchr",
 | 
			
		||||
 "regex-syntax",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "regex-syntax"
 | 
			
		||||
version = "0.8.4"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rustc-demangle"
 | 
			
		||||
version = "0.1.24"
 | 
			
		||||
@ -1355,6 +1393,7 @@ dependencies = [
 | 
			
		||||
 "minicrossterm",
 | 
			
		||||
 "nom",
 | 
			
		||||
 "piccolo",
 | 
			
		||||
 "regex",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 | 
			
		||||
@ -22,3 +22,4 @@ anyhow = "1.0.86"
 | 
			
		||||
serde = "1.0.209"
 | 
			
		||||
serde_json = "1.0.127"
 | 
			
		||||
gc-arena = { git = "https://github.com/kyren/gc-arena.git", rev = "5a7534b883b703f23cfb8c3cfdf033460aa77ea9" }
 | 
			
		||||
regex = "1.10.6"
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ use piccolo::{
 | 
			
		||||
    self, async_sequence, Callback, CallbackReturn, Context, FromValue, Function, IntoValue,
 | 
			
		||||
    SequenceReturn, Table, UserData, Value, Variadic,
 | 
			
		||||
};
 | 
			
		||||
use regex::Regex;
 | 
			
		||||
use std::{rc::Rc, str};
 | 
			
		||||
use yew::UseStateSetter;
 | 
			
		||||
 | 
			
		||||
@ -17,7 +18,35 @@ pub fn alias<'gc, 'a>(
 | 
			
		||||
    _global_memo: &'a GlobalMemoCell,
 | 
			
		||||
    _global_layout: &'a UseStateSetter<GlobalLayoutCell>,
 | 
			
		||||
) -> Callback<'gc> {
 | 
			
		||||
    Callback::from_fn(&ctx, move |_ctx, _ex, _stack| {
 | 
			
		||||
    Callback::from_fn(&ctx, move |ctx, _ex, mut stack| {
 | 
			
		||||
        let info: Table = ctx.get_global("info")?;
 | 
			
		||||
        let cur_frame: TermFrame =
 | 
			
		||||
            try_unwrap_frame(ctx, &info.get(ctx, ctx.intern_static(b"current_frame"))?)?;
 | 
			
		||||
        let frames: Table = ctx.get_global("frames")?;
 | 
			
		||||
        let cur_frame: Table = frames.get(ctx, cur_frame.0 as i64)?;
 | 
			
		||||
 | 
			
		||||
        let alias_match: piccolo::String = piccolo::String::from_value(
 | 
			
		||||
            ctx,
 | 
			
		||||
            stack
 | 
			
		||||
                .pop_front()
 | 
			
		||||
                .ok_or_else(|| anyhow::Error::msg("Missing alias match"))?,
 | 
			
		||||
        )?;
 | 
			
		||||
        let sub_to: piccolo::String = piccolo::String::from_value(
 | 
			
		||||
            ctx,
 | 
			
		||||
            stack
 | 
			
		||||
                .pop_front()
 | 
			
		||||
                .ok_or_else(|| anyhow::Error::msg("Missing substitution match"))?,
 | 
			
		||||
        )?;
 | 
			
		||||
        if !stack.is_empty() {
 | 
			
		||||
            Err(anyhow::Error::msg(
 | 
			
		||||
                "Extra arguments to alias command. Try wrapping the action in {}",
 | 
			
		||||
            ))?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let aliases: Table = cur_frame.get(ctx, "aliases")?;
 | 
			
		||||
 | 
			
		||||
        aliases.set(ctx, alias_match, sub_to)?;
 | 
			
		||||
 | 
			
		||||
        Ok(piccolo::CallbackReturn::Return)
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
@ -222,6 +251,9 @@ pub(super) fn new_frame<'gc>(ctx: Context<'gc>, _global_memo: &GlobalMemoCell) -
 | 
			
		||||
 | 
			
		||||
        frame_tab.set(ctx, ctx.intern_static(b"frame"), frame)?;
 | 
			
		||||
 | 
			
		||||
        let aliases_tab: Table = Table::new(&ctx);
 | 
			
		||||
        frame_tab.set(ctx, ctx.intern_static(b"aliases"), aliases_tab)?;
 | 
			
		||||
 | 
			
		||||
        Ok(piccolo::CallbackReturn::Return)
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
@ -257,6 +289,20 @@ pub(super) fn frame_input<'gc>(ctx: Context<'gc>, _global_memo: &GlobalMemoCell)
 | 
			
		||||
            .ok_or_else(|| anyhow::Error::msg("classes.frame:new missing line!"))?;
 | 
			
		||||
        stack.consume(ctx)?;
 | 
			
		||||
 | 
			
		||||
        // Check for an alias match...
 | 
			
		||||
        for (alias_match, alias_sub) in frame_tab.get::<&str, Table>(ctx, "aliases")?.iter() {
 | 
			
		||||
            if let Some(alias_match) = piccolo::String::from_value(ctx, alias_match)
 | 
			
		||||
                .ok()
 | 
			
		||||
                .and_then(|am| am.to_str().ok())
 | 
			
		||||
                .and_then(|v| Regex::new(v).ok())
 | 
			
		||||
            {
 | 
			
		||||
                if let Some(alias_sub) = piccolo::String::from_value(ctx, alias_sub)
 | 
			
		||||
                    .ok()
 | 
			
		||||
                    .and_then(|am| am.to_str().ok())
 | 
			
		||||
                {}
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let linked_mud = frame_tab.get_value(ctx, ctx.intern_static(b"linked_mud"));
 | 
			
		||||
        if linked_mud.is_nil() {
 | 
			
		||||
            return Ok(piccolo::CallbackReturn::Return);
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ pub mod command_handler;
 | 
			
		||||
pub mod id_intern;
 | 
			
		||||
pub mod lineengine;
 | 
			
		||||
pub mod lua_engine;
 | 
			
		||||
pub mod match_table;
 | 
			
		||||
pub mod parsing;
 | 
			
		||||
pub mod split_panel;
 | 
			
		||||
pub mod telnet;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										401
									
								
								src/match_table.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										401
									
								
								src/match_table.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,401 @@
 | 
			
		||||
use std::{collections::VecDeque, str::FromStr};
 | 
			
		||||
 | 
			
		||||
use anyhow::bail;
 | 
			
		||||
use itertools::Itertools;
 | 
			
		||||
use piccolo::{Context, IntoValue, Table, Value};
 | 
			
		||||
use regex::Regex;
 | 
			
		||||
 | 
			
		||||
use crate::parsing::{parse_commands, quote_string, ArgumentGuard, ParsedArgument, ParsedCommand};
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug)]
 | 
			
		||||
pub struct MatchSubTable {
 | 
			
		||||
    contents: Vec<MatchRecord>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl MatchSubTable {
 | 
			
		||||
    pub fn to_value<'gc>(&self, ctx: Context<'gc>) -> anyhow::Result<Value<'gc>> {
 | 
			
		||||
        let table = Table::new(&ctx);
 | 
			
		||||
        for record in self.contents.iter() {
 | 
			
		||||
            table.set(
 | 
			
		||||
                ctx,
 | 
			
		||||
                ctx.intern(record.match_text.as_bytes()),
 | 
			
		||||
                ctx.intern(record.sub_text.as_bytes()),
 | 
			
		||||
            )?;
 | 
			
		||||
        }
 | 
			
		||||
        Ok(table.into_value(ctx))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn try_sub(&self, input: &str) -> Option<Vec<ParsedCommand>> {
 | 
			
		||||
        for record in self.contents.iter() {
 | 
			
		||||
            if let Some(matched) = record.match_regex.captures(input) {
 | 
			
		||||
                let vec = Some(
 | 
			
		||||
                    record
 | 
			
		||||
                        .sub_commands
 | 
			
		||||
                        .iter()
 | 
			
		||||
                        .map(|subcmd| ParsedCommand {
 | 
			
		||||
                            arguments: subcmd
 | 
			
		||||
                                .arguments
 | 
			
		||||
                                .iter()
 | 
			
		||||
                                .map(|subarg| {
 | 
			
		||||
                                    let unquoted_text = subarg
 | 
			
		||||
                                        .text_parts
 | 
			
		||||
                                        .iter()
 | 
			
		||||
                                        .map(|tp| match tp {
 | 
			
		||||
                                            SubTextPart::Literal(t) => t.as_str(),
 | 
			
		||||
                                            SubTextPart::Variable(v) => {
 | 
			
		||||
                                                if let Ok(v) = <usize as FromStr>::from_str(v) {
 | 
			
		||||
                                                    matched.get(v).map_or("", |v| v.as_str())
 | 
			
		||||
                                                } else {
 | 
			
		||||
                                                    matched.name(v).map_or("", |v| v.as_str())
 | 
			
		||||
                                                }
 | 
			
		||||
                                            }
 | 
			
		||||
                                        })
 | 
			
		||||
                                        .join("");
 | 
			
		||||
                                    ParsedArgument {
 | 
			
		||||
                                        guard: if subarg.guard.is_none()
 | 
			
		||||
                                            && unquoted_text.contains(';')
 | 
			
		||||
                                        {
 | 
			
		||||
                                            Some(ArgumentGuard::Paren)
 | 
			
		||||
                                        } else {
 | 
			
		||||
                                            subarg.guard.clone()
 | 
			
		||||
                                        },
 | 
			
		||||
                                        text: unquoted_text.clone(),
 | 
			
		||||
                                        quoted_text: quote_string(&unquoted_text),
 | 
			
		||||
                                    }
 | 
			
		||||
                                })
 | 
			
		||||
                                .collect(),
 | 
			
		||||
                        })
 | 
			
		||||
                        .collect(),
 | 
			
		||||
                );
 | 
			
		||||
                return vec;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        None
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn add_record(&mut self, match_text: &str, sub_text: &str) -> anyhow::Result<()> {
 | 
			
		||||
        let rex = Regex::new(match_text)?;
 | 
			
		||||
 | 
			
		||||
        let parse_result = parse_commands(sub_text);
 | 
			
		||||
        let sub_commands: Vec<SubCommand> = parse_result
 | 
			
		||||
            .commands
 | 
			
		||||
            .into_iter()
 | 
			
		||||
            .map(|cmd| {
 | 
			
		||||
                Ok(SubCommand {
 | 
			
		||||
                    arguments: cmd
 | 
			
		||||
                        .arguments
 | 
			
		||||
                        .into_iter()
 | 
			
		||||
                        .map(parsedarg_to_subarg)
 | 
			
		||||
                        .collect::<anyhow::Result<VecDeque<SubArgument>>>()?,
 | 
			
		||||
                })
 | 
			
		||||
            })
 | 
			
		||||
            .collect::<anyhow::Result<Vec<SubCommand>>>()?;
 | 
			
		||||
 | 
			
		||||
        self.contents.push(MatchRecord {
 | 
			
		||||
            match_text: match_text.to_owned(),
 | 
			
		||||
            match_regex: rex,
 | 
			
		||||
            sub_text: sub_text.to_owned(),
 | 
			
		||||
            sub_commands,
 | 
			
		||||
        });
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn parsedarg_to_subarg(parsedarg: ParsedArgument) -> anyhow::Result<SubArgument> {
 | 
			
		||||
    let mut text_parts: Vec<SubTextPart> = vec![];
 | 
			
		||||
    let mut iter = parsedarg.text.chars().peekable();
 | 
			
		||||
    let mut buf = String::new();
 | 
			
		||||
    'outer: loop {
 | 
			
		||||
        match iter.next() {
 | 
			
		||||
            None => break 'outer,
 | 
			
		||||
            Some('$') => match iter.peek() {
 | 
			
		||||
                None => {
 | 
			
		||||
                    bail!("substitution ends in $ which is invalid.")
 | 
			
		||||
                }
 | 
			
		||||
                Some('$') => {
 | 
			
		||||
                    iter.next();
 | 
			
		||||
                    buf.push('$')
 | 
			
		||||
                }
 | 
			
		||||
                Some('{') => {
 | 
			
		||||
                    iter.next();
 | 
			
		||||
                    if !buf.is_empty() {
 | 
			
		||||
                        text_parts.push(SubTextPart::Literal(buf));
 | 
			
		||||
                    }
 | 
			
		||||
                    buf = String::new();
 | 
			
		||||
                    'inner: loop {
 | 
			
		||||
                        match iter.next() {
 | 
			
		||||
                            None => {
 | 
			
		||||
                                bail!("substitution opened with {{ is never closed.")
 | 
			
		||||
                            }
 | 
			
		||||
                            Some('}') => {
 | 
			
		||||
                                if buf.is_empty() {
 | 
			
		||||
                                    bail!("substitution of empty variable name.");
 | 
			
		||||
                                }
 | 
			
		||||
                                text_parts.push(SubTextPart::Variable(buf));
 | 
			
		||||
                                buf = String::new();
 | 
			
		||||
                                break 'inner;
 | 
			
		||||
                            }
 | 
			
		||||
                            Some(c) => buf.push(c),
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                Some(_) => {
 | 
			
		||||
                    if !buf.is_empty() {
 | 
			
		||||
                        text_parts.push(SubTextPart::Literal(buf));
 | 
			
		||||
                    }
 | 
			
		||||
                    buf = String::new();
 | 
			
		||||
                    'inner: loop {
 | 
			
		||||
                        match iter.peek() {
 | 
			
		||||
                            None => {
 | 
			
		||||
                                text_parts.push(SubTextPart::Variable(buf));
 | 
			
		||||
                                buf = String::new();
 | 
			
		||||
                                break 'outer;
 | 
			
		||||
                            }
 | 
			
		||||
                            Some(c) if *c == '_' || c.is_ascii_alphanumeric() => {
 | 
			
		||||
                                buf.push(*c);
 | 
			
		||||
                                iter.next();
 | 
			
		||||
                            }
 | 
			
		||||
                            Some(_) => {
 | 
			
		||||
                                if buf.is_empty() {
 | 
			
		||||
                                    bail!("substitution of empty variable name.");
 | 
			
		||||
                                }
 | 
			
		||||
                                text_parts.push(SubTextPart::Variable(buf));
 | 
			
		||||
                                buf = String::new();
 | 
			
		||||
                                break 'inner;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            Some(c) => buf.push(c),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if !buf.is_empty() {
 | 
			
		||||
        text_parts.push(SubTextPart::Literal(buf))
 | 
			
		||||
    }
 | 
			
		||||
    Ok(SubArgument {
 | 
			
		||||
        guard: parsedarg.guard,
 | 
			
		||||
        text_parts,
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct MatchRecord {
 | 
			
		||||
    pub match_text: String,
 | 
			
		||||
    pub match_regex: Regex,
 | 
			
		||||
    pub sub_text: String,
 | 
			
		||||
    // We parse into into commands and arguments upfront, before substitution.
 | 
			
		||||
    // This reduces the risk of security problems.
 | 
			
		||||
    pub sub_commands: Vec<SubCommand>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct SubCommand {
 | 
			
		||||
    pub arguments: VecDeque<SubArgument>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, PartialEq, Eq)]
 | 
			
		||||
pub struct SubArgument {
 | 
			
		||||
    pub guard: Option<ArgumentGuard>,
 | 
			
		||||
    pub text_parts: Vec<SubTextPart>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, PartialEq, Eq)]
 | 
			
		||||
pub enum SubTextPart {
 | 
			
		||||
    Literal(String),
 | 
			
		||||
    Variable(String),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::*;
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn parsedarg_to_subarg_works() {
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            parsedarg_to_subarg(ParsedArgument {
 | 
			
		||||
                guard: None,
 | 
			
		||||
                text: "hello world!".to_owned(),
 | 
			
		||||
                quoted_text: "hello world!".to_owned()
 | 
			
		||||
            })
 | 
			
		||||
            .unwrap(),
 | 
			
		||||
            SubArgument {
 | 
			
		||||
                guard: None,
 | 
			
		||||
                text_parts: vec![SubTextPart::Literal("hello world!".to_owned())]
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            parsedarg_to_subarg(ParsedArgument {
 | 
			
		||||
                guard: None,
 | 
			
		||||
                text: "hello $adjective ${my world}".to_owned(),
 | 
			
		||||
                quoted_text: "hello $adjective ${my world}".to_owned()
 | 
			
		||||
            })
 | 
			
		||||
            .unwrap(),
 | 
			
		||||
            SubArgument {
 | 
			
		||||
                guard: None,
 | 
			
		||||
                text_parts: vec![
 | 
			
		||||
                    SubTextPart::Literal("hello ".to_owned()),
 | 
			
		||||
                    SubTextPart::Variable("adjective".to_owned()),
 | 
			
		||||
                    SubTextPart::Literal(" ".to_owned()),
 | 
			
		||||
                    SubTextPart::Variable("my world".to_owned()),
 | 
			
		||||
                ]
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            parsedarg_to_subarg(ParsedArgument {
 | 
			
		||||
                guard: Some(ArgumentGuard::DoubleQuote),
 | 
			
		||||
                text: "hello $adjective${my world}${your world} end".to_owned(),
 | 
			
		||||
                quoted_text: "hello $adjective$\\{my world\\}$\\{your world\\} end".to_owned()
 | 
			
		||||
            })
 | 
			
		||||
            .unwrap(),
 | 
			
		||||
            SubArgument {
 | 
			
		||||
                guard: Some(ArgumentGuard::DoubleQuote),
 | 
			
		||||
                text_parts: vec![
 | 
			
		||||
                    SubTextPart::Literal("hello ".to_owned()),
 | 
			
		||||
                    SubTextPart::Variable("adjective".to_owned()),
 | 
			
		||||
                    SubTextPart::Variable("my world".to_owned()),
 | 
			
		||||
                    SubTextPart::Variable("your world".to_owned()),
 | 
			
		||||
                    SubTextPart::Literal(" end".to_owned()),
 | 
			
		||||
                ]
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn parsedarg_rejects_invalid() {
 | 
			
		||||
        assert!(parsedarg_to_subarg(ParsedArgument {
 | 
			
		||||
            guard: None,
 | 
			
		||||
            text: "${untermin".to_owned(),
 | 
			
		||||
            quoted_text: "${untermin".to_owned()
 | 
			
		||||
        })
 | 
			
		||||
        .is_err());
 | 
			
		||||
        assert!(parsedarg_to_subarg(ParsedArgument {
 | 
			
		||||
            guard: None,
 | 
			
		||||
            text: "$foo$".to_owned(),
 | 
			
		||||
            quoted_text: "$foo$".to_owned()
 | 
			
		||||
        })
 | 
			
		||||
        .is_err());
 | 
			
		||||
        assert!(parsedarg_to_subarg(ParsedArgument {
 | 
			
		||||
            guard: None,
 | 
			
		||||
            text: "$ hello".to_owned(),
 | 
			
		||||
            quoted_text: "$ hello".to_owned()
 | 
			
		||||
        })
 | 
			
		||||
        .is_err());
 | 
			
		||||
        assert!(parsedarg_to_subarg(ParsedArgument {
 | 
			
		||||
            guard: None,
 | 
			
		||||
            text: "My name is ${}".to_owned(),
 | 
			
		||||
            quoted_text: "My name is ${}".to_owned()
 | 
			
		||||
        })
 | 
			
		||||
        .is_err());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn matchsubtable_works() {
 | 
			
		||||
        let mut table: MatchSubTable = Default::default();
 | 
			
		||||
        table
 | 
			
		||||
            .add_record(
 | 
			
		||||
                "^foo (?<bar>[a-z]+) baz",
 | 
			
		||||
                "\\\"Someone is talking $bar about foo baz?;:flexes his ${bar}",
 | 
			
		||||
            )
 | 
			
		||||
            .expect("adding record failed");
 | 
			
		||||
        assert_eq!(table.try_sub("unrelated babble"), None);
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            table.try_sub("foo woots baz\r\n"),
 | 
			
		||||
            Some(vec![
 | 
			
		||||
                ParsedCommand {
 | 
			
		||||
                    arguments: [
 | 
			
		||||
                        ParsedArgument {
 | 
			
		||||
                            guard: None,
 | 
			
		||||
                            text: "\"Someone".to_owned(),
 | 
			
		||||
                            quoted_text: "\\\"Someone".to_owned()
 | 
			
		||||
                        },
 | 
			
		||||
                        ParsedArgument {
 | 
			
		||||
                            guard: None,
 | 
			
		||||
                            text: "is".to_owned(),
 | 
			
		||||
                            quoted_text: "is".to_owned()
 | 
			
		||||
                        },
 | 
			
		||||
                        ParsedArgument {
 | 
			
		||||
                            guard: None,
 | 
			
		||||
                            text: "talking".to_owned(),
 | 
			
		||||
                            quoted_text: "talking".to_owned()
 | 
			
		||||
                        },
 | 
			
		||||
                        ParsedArgument {
 | 
			
		||||
                            guard: None,
 | 
			
		||||
                            text: "woots".to_owned(),
 | 
			
		||||
                            quoted_text: "woots".to_owned()
 | 
			
		||||
                        },
 | 
			
		||||
                        ParsedArgument {
 | 
			
		||||
                            guard: None,
 | 
			
		||||
                            text: "about".to_owned(),
 | 
			
		||||
                            quoted_text: "about".to_owned()
 | 
			
		||||
                        },
 | 
			
		||||
                        ParsedArgument {
 | 
			
		||||
                            guard: None,
 | 
			
		||||
                            text: "foo".to_owned(),
 | 
			
		||||
                            quoted_text: "foo".to_owned()
 | 
			
		||||
                        },
 | 
			
		||||
                        ParsedArgument {
 | 
			
		||||
                            guard: None,
 | 
			
		||||
                            text: "baz?".to_owned(),
 | 
			
		||||
                            quoted_text: "baz?".to_owned()
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                    .into()
 | 
			
		||||
                },
 | 
			
		||||
                ParsedCommand {
 | 
			
		||||
                    arguments: [
 | 
			
		||||
                        ParsedArgument {
 | 
			
		||||
                            guard: None,
 | 
			
		||||
                            text: ":flexes".to_owned(),
 | 
			
		||||
                            quoted_text: ":flexes".to_owned()
 | 
			
		||||
                        },
 | 
			
		||||
                        ParsedArgument {
 | 
			
		||||
                            guard: None,
 | 
			
		||||
                            text: "his".to_owned(),
 | 
			
		||||
                            quoted_text: "his".to_owned()
 | 
			
		||||
                        },
 | 
			
		||||
                        ParsedArgument {
 | 
			
		||||
                            guard: None,
 | 
			
		||||
                            text: "woots".to_owned(),
 | 
			
		||||
                            quoted_text: "woots".to_owned()
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                    .into()
 | 
			
		||||
                }
 | 
			
		||||
            ])
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn matchsubtable_resists_command_injection() {
 | 
			
		||||
        let mut table: MatchSubTable = Default::default();
 | 
			
		||||
        table
 | 
			
		||||
            .add_record("^foo (.*)", "safe_command $1")
 | 
			
		||||
            .expect("adding record failed");
 | 
			
		||||
        let result = table
 | 
			
		||||
            .try_sub("foo pwned!};dangerous_command {")
 | 
			
		||||
            .expect("didn't match");
 | 
			
		||||
        let expected = ParsedCommand {
 | 
			
		||||
            arguments: [
 | 
			
		||||
                ParsedArgument {
 | 
			
		||||
                    guard: None,
 | 
			
		||||
                    text: "safe_command".to_owned(),
 | 
			
		||||
                    quoted_text: "safe_command".to_owned(),
 | 
			
		||||
                },
 | 
			
		||||
                ParsedArgument {
 | 
			
		||||
                    guard: Some(ArgumentGuard::Paren),
 | 
			
		||||
                    text: "pwned!};dangerous_command {".to_owned(),
 | 
			
		||||
                    quoted_text: "pwned!\\};dangerous_command \\{".to_owned(),
 | 
			
		||||
                },
 | 
			
		||||
            ]
 | 
			
		||||
            .into(),
 | 
			
		||||
        };
 | 
			
		||||
        assert_eq!(result, vec![expected.clone()]);
 | 
			
		||||
        let ser_result = result[0].to_string();
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            ser_result,
 | 
			
		||||
            "safe_command {pwned!\\};dangerous_command \\{}".to_owned()
 | 
			
		||||
        );
 | 
			
		||||
        assert_eq!(parse_commands(&ser_result).commands, vec![expected]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -140,6 +140,20 @@ fn unquote_string(input: &str) -> String {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn quote_string(input: &str) -> String {
 | 
			
		||||
    let mut buf: String = String::new();
 | 
			
		||||
    for c in input.chars() {
 | 
			
		||||
        match c {
 | 
			
		||||
            '\\' => buf.push_str("\\\\"),
 | 
			
		||||
            '{' => buf.push_str("\\{"),
 | 
			
		||||
            '}' => buf.push_str("\\}"),
 | 
			
		||||
            '"' => buf.push_str("\\\""),
 | 
			
		||||
            c => buf.push(c),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    buf
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn parse_string(input: &str) -> IResult<&str, ()> {
 | 
			
		||||
    value(
 | 
			
		||||
        (),
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user