dbus 0.9.6

Bindings to D-Bus, which is a bus commonly used on Linux for inter-process communication.
Documentation
use std::error::Error as stdError;
use std::fmt::{Debug, Formatter, Display};
use crate::message::MatchRule;
use crate::{MessageType, Path};
use std::convert::TryFrom;
use crate::strings::{Interface, BusName, Member};

// Our grammar:
// rules: rule (, rule)*
// rule: sender | type | interface | member | path | path_namespace | destination | arg | arg_path
// bool : 'true' | 'false'
// type: "type" "=" message_type
// message_type: "'signal'" | "'method_call'" | "'method_return'" | "'error'"
// sender: "sender" "=" string
// interface: "interface" "=" string
// member: "member" "=" string
// path: "path" "=" string
// path_namespace: "path_namespace" "=" string
// destination: "destination" "=" string
// arg: "arg" 0-63 "=" string
// arg_path: "arg" 0-63 "path" "=" string
// eavesdrop: "eavesdrop" "=" bool


#[derive(Clone, Debug)]
/// Error type that covers errors that might happen during parsing.
pub enum Error {
    /// The type specified in the match rule is unknown
    UnknownType,
    /// The key is wrong / unsupported
    UnknownKey,
    /// Boolean could not be parsed
    BadBoolean,
    /// Error that occured while converting a string to a DBus format
    BadConversion(String),
}

impl Display for Error {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "Error while parsing MatchRule: ")?;
        match self {
            Error::UnknownType => {
                write!(f, "Unsupported message type")
            }
            Error::UnknownKey => {
                write!(f, "Unknown key used")
            }
            Error::BadBoolean => {
                write!(f, "Got bad boolean value")
            }
            Error::BadConversion(err) => {
                write!(f, "Error while converting: {}", err)
            }
        }
    }
}

impl stdError for Error {}

/// Key-Value-pair
pub type TokenRule<'a> = (&'a str, &'a str);
/// Fixed size buffer for match rule tokens
pub type TokenBuffer<'a> = Vec<TokenRule<'a>>;

#[derive(Clone, Debug)]
/// Tokenizes a match rule into key-value-pairs
struct Tokenizer<'a> {
    text: &'a str,
}

impl<'a> Tokenizer<'a> {
    /// Builds a new tokenizer for the input &str
    pub fn new(text: &'a str) -> Self {
        Self {
            text,
        }
    }

    /// Parse the key part of the key-value-pair. This is rather easy as all keys are rather
    /// easily defined and may only contain `[a-z0-9]` so we can simply split at `'='`.
    fn key(&self) -> (&'a str, &'a str) {
        let index = self.text.find('=').unwrap_or_else(|| self.text.len());

        ((&self.text[..index]).trim(), &self.text[index + 1..])
    }

    /// Parses values as generic strings.
    /// This does not do any validation (yet) with regards to supported characters.
    fn value(&self) -> (&'a str, &'a str) {
        let mut i = 0;
        let mut quoted = false;
        let mut escape = false;

        for c in self.text.chars() {
            match c {
                '\'' if !escape => {
                    quoted = !quoted;
                }
                ',' if !quoted => {
                    break;
                }
                '\\' if !quoted => {
                    escape = true;
                    i += 1;
                    continue;
                }
                _ => {}
            }
            escape = false;

            i += 1;
        }

        // Skip comma if there is still space in the buffer
        let j = if self.text.len() == i { i } else { i + 1 };
        ((&self.text[..i]).trim(), &self.text[j..])
    }

    /// Tokenizes a string into key-value-pairs
    pub fn tokenize(&mut self) -> Result<TokenBuffer<'a>, Error> {
        let mut rules = TokenBuffer::new();

        while !self.text.is_empty() {
            let (key, rest) = self.key();
            self.text = rest;
            let (value, rest) = self.value();
            self.text = rest;
            rules.push((key, value))
        }
        Ok(rules)
    }
}

#[derive(Clone, Debug)]
/// Helper struct for parsing MatchRule's
pub struct Parser<'a> {
    tokens: TokenBuffer<'a>,
}

impl<'a> Parser<'a> {
    /// Builds a new parser after tokenizing the input string `text`.
    pub fn new(text: &'a str) -> Result<Self, Error> {
        Ok(Self {
            tokens: Tokenizer::new(text).tokenize()?
        })
    }

    /// Cleans a string from the string syntax allowed by DBus. This includes concatenating
    /// things like `''\'''` to `'`. DBus strings sort of work like in a POSIX shell and
    /// concatenation is implied. There is only one escape sequence and that is in an unquoted
    /// substring a backslash may escape an ASCII apostrophe (U+0027).
    /// This method is the only one within the parser that allocates and in theory could be
    /// rewritten to taking a `&mut str` instead of returning a `String` since all
    /// strings are |output| <= |buf.len()|.
    fn clean_string(&self, buf: &str) -> String {
        let mut quoted = false;
        let mut escape = false;
        let mut outbuf = String::with_capacity(buf.len());

        for c in buf.chars() {
            match c {
                '\'' if !escape => {
                    quoted = !quoted;
                }
                '\\' if !quoted => {
                    escape = true;
                    continue;
                }
                c if c.is_whitespace() && !quoted => {
                    continue;
                }
                c => {
                    outbuf.push(c);
                }
            }
        }

        outbuf
    }

    /// Parses key-value-pair tokens into a MatchRule
    pub fn parse(&self) -> Result<MatchRule<'a>, Error> {
        let mut match_rule = MatchRule::new();

        for &(key, raw_value) in &self.tokens {
            let value = self.clean_string(raw_value);
            match key {
                "type" => {
                    match_rule = match_rule.with_type(MessageType::try_from(value.as_str()).map_err(|_| Error::UnknownType)?);
                    Ok(())
                }
                "interface" => {
                    match_rule.interface = Some(Interface::new(value).map_err(Error::BadConversion)?);
                    Ok(())
                }
                "sender" => {
                    match_rule.sender = Some(BusName::new(value).map_err(Error::BadConversion)?);
                    Ok(())
                }
                "member" => {
                    match_rule.member = Some(Member::new(value).map_err(Error::BadConversion)?);
                    Ok(())
                }
                "path" => {
                    match_rule.path = Some(Path::new(value).map_err(Error::BadConversion)?);
                    Ok(())
                }
                "path_namespace" => {
                    match_rule.path = Some(Path::new(value).map_err(Error::BadConversion)?);
                    match_rule.path_is_namespace = true;
                    Ok(())
                }
                "eavesdrop" => {
                    match raw_value {
                        "'true'" | "true" => {
                            match_rule = match_rule.with_eavesdrop();
                            Ok(())
                        }
                        "'false'" | "false" => {
                            Ok(())
                        }
                        _ => {
                            Err(Error::BadBoolean)
                        }
                    }
                }
                _ => {
                    // Args and Destination are not supported yet.
                    Err(Error::UnknownKey)
                }
            }?;
        }

        Ok(match_rule)
    }
}

#[cfg(test)]
mod tests {
    use crate::message::parser::Error;
    use crate::message::MatchRule;

    #[test]
    fn test_tokenizer() -> Result<(), Error> {
        let mr = MatchRule::parse(r"interface='org.freedesktop.Notifications',member='Notify'")?;
        assert_eq!(mr.match_str(), "interface='org.freedesktop.Notifications',member='Notify'");
        let mr = MatchRule::parse(r"interface='org.mpris.MediaPlayer2.Player' , path= /org/mpris/MediaPlayer2,member='Notify', eavesdrop ='true'")?;
        assert_eq!(mr.match_str(), "path='/org/mpris/MediaPlayer2',interface='org.mpris.MediaPlayer2.Player',member='Notify',eavesdrop='true'");
        Ok(())
    }

    #[test]
    fn test_malformed() {
        assert!(MatchRule::parse(r"interface='org.freedesktop.Notifications',member=").is_err());
    }

    #[test]
    fn test_spurious_comma() {
        assert!(MatchRule::parse(r"interface='org.freedesktop.Notifications',").is_ok());
    }
}