javelin-pattern 0.1.2

Rust bindings to Jeffrey Lim's JavelinPattern regular expression engine.
Documentation
use std::{
    ops::ControlFlow,
    panic::{AssertUnwindSafe, catch_unwind},
};

use javelin_pattern::{
    Error,
    bytes::{Regex, RegexBuilder},
};

#[test]
fn matches_and_captures_borrow_the_input() {
    let input = b"before answer=42 after";
    let (matched, captures) = {
        let re = Regex::new(r"(\w+)=(\d+)").unwrap();
        assert_eq!(re.as_str(), r"(\w+)=(\d+)");
        assert_eq!(re.captures_len(), 3);
        assert!(re.is_match(input));
        assert!(!re.is_full_match(input));
        assert!(re.is_full_match(b"answer=42"));
        (re.find(input).unwrap(), re.captures(input).unwrap())
    };
    assert_eq!(matched.range(), 7..16);
    assert_eq!(matched.as_bytes(), b"answer=42");
    assert_eq!(captures.get(1).unwrap().as_bytes(), b"answer");
    assert_eq!(captures.get(2).unwrap().as_bytes(), b"42");
    assert_eq!(captures.iter().count(), 3);
    assert!(!captures.is_empty());
    assert!(captures.get(3).is_none());
}

#[test]
fn binary_input_and_compile_errors() {
    let re = Regex::new(r"\x00(\xFF)").unwrap();
    let input = b"x\0\xffy";
    assert_eq!(re.find(input).unwrap().range(), 1..3);
    assert_eq!(
        re.captures(input).unwrap().get(1).unwrap().as_bytes(),
        b"\xff"
    );
    assert_eq!(Regex::new("a\0b").unwrap_err(), Error::NulByte(1));
    let error = Regex::new("(").unwrap_err();
    assert!(error.code().unwrap() > 0);
    assert!(error.to_string().contains("compilation failed"));
}

#[test]
fn offsets_preserve_context_and_handle_end_of_input() {
    let re = Regex::new(r"(?<=a)b").unwrap();
    assert!(re.is_match_at(b"ab", 1));
    assert_eq!(re.find_at(b"ab", 1).unwrap().range(), 1..2);
    assert_eq!(
        re.captures_at(b"ab", 1).unwrap().get(0).unwrap().range(),
        1..2
    );
    assert!(!Regex::new(r"\Ab").unwrap().is_match_at(b"ab", 1));
    for offset in [3, usize::MAX] {
        assert!(!re.is_match_at(b"ab", offset));
        assert!(re.find_at(b"ab", offset).is_none());
        assert!(re.captures_at(b"ab", offset).is_none());
        assert_eq!(
            re.scan_at(b"ab", offset, |_| -> ControlFlow<()> {
                panic!("unexpected match")
            }),
            ControlFlow::Continue(())
        );
        assert_eq!(
            re.scan_captures_at(b"ab", offset, |_| -> ControlFlow<()> {
                panic!("unexpected match")
            }),
            ControlFlow::Continue(())
        );
    }
    let empty = Regex::new("").unwrap();
    assert_eq!(empty.find(b"").unwrap().range(), 0..0);
    assert!(empty.is_match(b""));
    assert!(empty.is_full_match(b""));
    assert_eq!(empty.captures(b"").unwrap().get(0).unwrap().as_bytes(), b"");
    assert_eq!(empty.find_at(b"ab", 2).unwrap().range(), 2..2);
}

#[test]
fn empty_matches_retry_nonempty_alternatives() {
    let re = Regex::new("a*?").unwrap();
    let mut ranges = Vec::new();
    assert_eq!(
        re.scan(b"a", |matched| {
            ranges.push(matched.range());
            ControlFlow::<()>::Continue(())
        }),
        ControlFlow::Continue(())
    );
    assert_eq!(ranges, [0..0, 0..1, 1..1]);
    let mut captures = Vec::new();
    assert_eq!(
        re.scan_captures(b"a", |capture| {
            captures.push(capture);
            ControlFlow::<()>::Continue(())
        }),
        ControlFlow::Continue(())
    );
    assert_eq!(
        captures
            .iter()
            .map(|c| c.get(0).unwrap().range())
            .collect::<Vec<_>>(),
        ranges
    );
}

#[test]
fn scans_clear_unmatched_groups_and_retain_empty_groups() {
    let re = Regex::new("(a)|(b)()").unwrap();
    let mut captures = Vec::new();
    assert_eq!(
        re.scan_captures(b"ab", |capture| {
            captures.push(capture);
            ControlFlow::<()>::Continue(())
        }),
        ControlFlow::Continue(())
    );
    assert_eq!(captures[0].get(1).unwrap().range(), 0..1);
    assert!(captures[0].get(2).is_none());
    assert!(captures[0].get(3).is_none());
    assert!(captures[1].get(1).is_none());
    assert_eq!(captures[1].get(2).unwrap().range(), 1..2);
    assert_eq!(captures[1].get(3).unwrap().range(), 2..2);
}

#[test]
fn early_stopping_returns_payload_and_allows_nested_scans() {
    let re = Regex::new("a").unwrap();
    let mut calls = 0;
    let result = re.scan(b"aaa", |matched| {
        calls += 1;
        assert_eq!(re.find(b"ba").unwrap().range(), 1..2);
        if calls == 2 {
            ControlFlow::Break(matched.range())
        } else {
            ControlFlow::Continue(())
        }
    });
    assert_eq!(result, ControlFlow::Break(1..2));
    assert_eq!(calls, 2);
    assert_eq!(
        re.scan(b"a", |_| ControlFlow::Break(String::from("final"))),
        ControlFlow::Break(String::from("final"))
    );
    assert_eq!(
        re.scan_captures(b"aaa", |c| ControlFlow::Break(c.get(0).unwrap().range())),
        ControlFlow::Break(0..1)
    );
}

#[test]
fn callback_panics_resume_in_rust_and_leave_regex_usable() {
    let re = Regex::new("a").unwrap();
    let panic = catch_unwind(AssertUnwindSafe(|| {
        re.scan(b"aa", |_| -> ControlFlow<()> {
            std::panic::panic_any(123usize)
        })
    }))
    .unwrap_err();
    assert_eq!(panic.downcast_ref::<usize>(), Some(&123));
    let panic = catch_unwind(AssertUnwindSafe(|| {
        re.scan_captures(b"aa", |_| -> ControlFlow<()> {
            std::panic::panic_any(String::from("capture panic"))
        })
    }))
    .unwrap_err();
    assert_eq!(panic.downcast_ref::<String>().unwrap(), "capture panic");
    assert!(re.is_full_match(b"a"));
}

#[test]
fn builder_options_and_unicode_byte_offsets() {
    let re = RegexBuilder::new()
        .caseless(true)
        .multi_line(true)
        .build("^abc$")
        .unwrap();
    assert_eq!(re.find(b"x\nABC\ny").unwrap().range(), 2..5);
    assert!(
        RegexBuilder::new()
            .dotall(true)
            .build("a.b")
            .unwrap()
            .is_full_match(b"a\nb")
    );
    assert!(
        !RegexBuilder::new()
            .caseless(true)
            .caseless(false)
            .build("a")
            .unwrap()
            .is_match(b"A")
    );
    assert_eq!(
        RegexBuilder::new()
            .ungreedy(true)
            .build("a+")
            .unwrap()
            .find(b"aaa")
            .unwrap()
            .range(),
        0..1
    );
    assert!(
        !RegexBuilder::new()
            .anchored(true)
            .build("a")
            .unwrap()
            .is_match(b"ba")
    );
    assert!(
        RegexBuilder::new()
            .glob(true)
            .build("*.txt")
            .unwrap()
            .is_full_match(b"file.txt")
    );

    let re = RegexBuilder::new()
        .utf(true)
        .ucp(true)
        .build(r"\w+")
        .unwrap();
    let input = "!αβ!".as_bytes();
    assert_eq!(re.find(input).unwrap().range(), 1..5);
    assert_eq!(
        re.captures(input).unwrap().get(0).unwrap().as_bytes(),
        "αβ".as_bytes()
    );
    let re = RegexBuilder::new()
        .utf(true)
        .caseless(true)
        .unicode_case(true)
        .build("Σ")
        .unwrap();
    assert!(re.is_full_match("σ".as_bytes()));
    let re = RegexBuilder::new().utf(true).build("").unwrap();
    let mut ranges = Vec::new();
    assert_eq!(
        re.scan("é".as_bytes(), |matched| {
            ranges.push(matched.range());
            ControlFlow::<()>::Continue(())
        }),
        ControlFlow::Continue(())
    );
    assert_eq!(ranges, [0..0, 2..2]);
}

#[test]
fn captures_report_reset_match_start() {
    let re = Regex::new(r"a\Kb").unwrap();
    assert_eq!(re.find(b"ab").unwrap().range(), 1..2);
    assert_eq!(re.captures(b"ab").unwrap().get(0).unwrap().range(), 1..2);
}

#[test]
fn clones_share_a_regex_across_threads() {
    fn send_sync<T: Send + Sync>() {}
    send_sync::<Regex>();
    let re = Regex::new(r"(a+)|(b+)").unwrap();
    let threads: Vec<_> = (0..4)
        .map(|_| {
            let re = re.clone();
            std::thread::spawn(move || {
                for _ in 0..32 {
                    assert_eq!(re.find(b"xxaaabb").unwrap().range(), 2..5);
                    let captures = re.captures(b"bb").unwrap();
                    assert!(captures.get(1).is_none());
                    assert_eq!(captures.get(2).unwrap().as_bytes(), b"bb");
                }
            })
        })
        .collect();
    drop(re);
    for thread in threads {
        thread.join().unwrap();
    }
}

#[test]
fn unused_captures_can_be_removed_without_losing_backreferences() {
    for backtracking in [false, true] {
        let mut builder = RegexBuilder::new();
        builder.prefer_backtracking(backtracking).auto_cluster(true);
        let re = builder.build("(a)(b)").unwrap();
        assert_eq!(re.captures_len(), 1);
        assert!(re.is_full_match(b"ab"));

        let re = builder.build(r"(a)(b)\2").unwrap();
        assert!(re.is_full_match(b"abb"));
        assert!(!re.is_full_match(b"aba"));

        let re = builder.build("(a*?)").unwrap();
        let mut ranges = Vec::new();
        assert_eq!(
            re.scan(b"a", |matched| {
                ranges.push(matched.range());
                ControlFlow::<()>::Continue(())
            }),
            ControlFlow::Continue(())
        );
        assert_eq!(ranges, [0..0, 0..1, 1..1]);

        let re = builder
            .auto_cluster(false)
            .prefer_backtracking(false)
            .build("(a)(b)")
            .unwrap();
        assert_eq!(re.captures_len(), 3);
        assert_eq!(re.captures(b"ab").unwrap().get(2).unwrap().as_bytes(), b"b");
    }
}