riam 0.1.0

A IAM inspired policy engine for making authorization decisions
Documentation
/// Test whether the incoming pattern string 's' matches the given pattern string 'pattern'.
///
/// A wildcard '*' character may appear anywhere in the `pattern`. A wildcard character matches
/// any character any number of times until the next character in the pattern is seen. If the
/// wildcard is the last char in the pattern then the remaining input string must be a match.
///
/// e.g. `abc*xyz` matches `abcdefghijkxyz`
///
/// If no wildcard is present in the pattern the inputs must be an exact match.
///
pub(crate) fn matches(pattern: &str, s: &str) -> bool {
    let mut piter = pattern.chars();
    let mut siter = s.chars();

    let mut pc = piter.next();
    let mut sc = siter.next();

    while pc.is_some() && sc.is_some() {
        let pcurr = pc.unwrap();
        let scurr = sc.unwrap();

        // Case 1: '*' is seen in pattern
        //     look at next char in pattern if any
        //     - if any chars left, grab that char and consume chars until that char is seen, if we exhaust 's' then no match
        //     - if none, short circuit. pattern ends in a '*' and matches the remaining string whatever it is
        if pcurr == '*' {
            pc = piter.next();
            // peek ahead
            if let Some(nc) = pc {
                if nc == '*' {
                    // special case '**', consume first '*' in pattern and cont
                    continue;
                }

                // consume chars in sc until that char is found or it is exhausted
                let pnext = pc.unwrap();
                while sc.is_some() && sc != Some(pnext) {
                    sc = siter.next();
                }
            } else {
                // pattern is exhausted and ends in a '*', short circuit because it matches anything remaining in 's'
                return true;
            }
        } else if pcurr != scurr {
            // Case 2: both pattern and incoming 's' chars are regular chars, compare 1-1 for equality
            return false;
        }

        pc = piter.next();
        sc = siter.next();
    }

    // one or more iterators is exhausted
    if pc.is_some() || sc.is_some() {
        return false;
    }

    true
}

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

    #[test]
    fn test_matches() {
        let cases = vec![
            ("resources:blog:123", "resources:blog:123", true),
            ("re*123", "resources:blog:123", true),
            ("resources:blog:*", "resources:blog:123", true),
            ("resources:*", "resources:blog:123", true),
            ("resources:blog:123", "resources:blog:789", false),
            ("accounts:123", "resources:blog:789", false),
            ("actions:*:123", "actions:accounts:list:123", false),
            ("actions:*:list:*", "actions:accounts:list:123", true),
            ("actions:*:*:123", "actions:accounts:list:123", true),
            ("actions:**123", "actions:accounts:list:123", true),
        ];

        for x in cases {
            let (pattern, input, expected) = x;
            let actual = matches(pattern, input);
            assert_eq!(expected, actual, "pattern: {}, input: {}", pattern, input);
        }
    }

    #[bench]
    fn bench_match_wildcard(b: &mut Bencher) {
        let pattern = "actions:*:list:123";
        let input = "actions:accounts:list:123";
        b.iter(|| matches(pattern, input));
    }

    #[bench]
    fn bench_match_exact(b: &mut Bencher) {
        let pattern = "actions:accounts:list:123";
        let input = "actions:accounts:list:123";
        b.iter(|| matches(pattern, input));
    }
}