rudof_rdf/rdf_core/utils/regex.rs
1use std::{borrow::Cow, fmt::Display};
2
3use regex::{Regex, RegexBuilder};
4use thiserror::Error;
5
6/// Maximum size limit for compiled regular expressions.
7const REGEX_SIZE_LIMIT: usize = 1_000_000;
8
9/// A regular expression for RDF and SPARQL pattern matching.
10///
11/// This type wraps the Rust [`Regex`] type with SPARQL/XPath-compatible flag
12/// handling. It stores both the compiled regex and the original pattern with
13/// flags for display and debugging purposes.
14#[derive(Debug, Clone)]
15pub struct RDFRegex {
16 /// The compiled regular expression.
17 regex: Regex,
18 /// The source pattern string (after flag processing).
19 source: String,
20 /// Optional flags that were applied to this regex.
21 flags: Option<String>,
22}
23
24impl RDFRegex {
25 /// Creates a new regular expression with SPARQL-compatible flags.
26 ///
27 /// This constructor builds a regex following the XPath/SPARQL flag semantics,
28 /// which differ slightly from standard Rust regex flags. The implementation
29 /// is inspired by the Oxigraph SPARQL engine (https://github.com/oxigraph/oxigraph/blob/main/lib/spareval/src/eval.rs).
30 ///
31 /// # Arguments
32 ///
33 /// * `pattern` - The regular expression pattern string
34 /// * `flags` - Optional string containing flag characters (see below)
35 ///
36 /// # Supported Flags
37 ///
38 /// Following the [XPath specification](https://www.w3.org/TR/xpath-functions/#flags):
39 ///
40 /// - **`s`** (dot-all): Makes `.` match any character including newlines
41 /// - **`m`** (multi-line): Makes `^` and `$` match line boundaries, not just string boundaries
42 /// - **`i`** (case-insensitive): Performs case-insensitive matching
43 /// - **`x`** (ignore whitespace): Ignores unescaped whitespace and enables comments with `#`
44 /// - **`q`** (quote/literal): Treats the entire pattern as a literal string (escapes special characters)
45 ///
46 /// Flags can be combined, e.g., `"im"` for case-insensitive multi-line matching.
47 ///
48 /// # Size Limits
49 ///
50 /// The compiled regex is limited to [`REGEX_SIZE_LIMIT`] bytes. Patterns
51 /// exceeding this limit will fail with an error.
52 ///
53 /// # Errors
54 ///
55 /// Returns an error if:
56 /// - The pattern syntax is invalid (e.g., unbalanced parentheses, invalid escape sequences)
57 /// - An unsupported flag character is provided
58 /// - The compiled regex exceeds the size limit
59 pub fn new(pattern: &str, flags: Option<&str>) -> Result<Self, RDFRegexError> {
60 let mut pattern = Cow::Borrowed(pattern);
61 let flags = flags.unwrap_or_default();
62 if flags.contains('q') {
63 pattern = regex::escape(&pattern).into();
64 }
65 let mut regex_builder = RegexBuilder::new(&pattern);
66 regex_builder.size_limit(REGEX_SIZE_LIMIT);
67 for flag in flags.chars() {
68 match flag {
69 's' => {
70 regex_builder.dot_matches_new_line(true);
71 },
72 'm' => {
73 regex_builder.multi_line(true);
74 },
75 'i' => {
76 regex_builder.case_insensitive(true);
77 },
78 'x' => {
79 regex_builder.ignore_whitespace(true);
80 },
81 'q' => (),
82 _ => return Err(RDFRegexError::InvalidFlagOption(flag)),
83 }
84 }
85 let regex = regex_builder.build()?;
86 Ok(RDFRegex {
87 regex,
88 source: pattern.into_owned(),
89 flags: if flags.is_empty() {
90 None
91 } else {
92 Some(flags.to_string())
93 },
94 })
95 }
96
97 /// Tests whether the regex matches the given text.
98 ///
99 /// Returns `true` if the pattern matches any part of the input string,
100 /// following standard regex semantics (not anchored by default).
101 ///
102 /// # Arguments
103 ///
104 /// * `text` - The string to test against the pattern
105 pub fn is_match(&self, text: &str) -> bool {
106 self.regex.is_match(text)
107 }
108
109 /// Returns the flags used to create this regex, if any.
110 pub fn flags(&self) -> Option<&str> {
111 self.flags.as_deref()
112 }
113
114 /// Returns the source pattern string.
115 ///
116 /// For patterns created with the `q` (quote) flag, this returns the
117 /// escaped version of the original pattern.
118 pub fn source(&self) -> &str {
119 &self.source
120 }
121}
122
123/// Errors that can occur when creating or using RDF regular expressions.
124#[derive(Error, Debug)]
125pub enum RDFRegexError {
126 /// The regex pattern has invalid syntax.
127 #[error("Invalid regex pattern: {0}")]
128 InvalidPattern(#[from] regex::Error),
129
130 /// An unsupported flag character was provided.
131 ///
132 /// Valid flags are: `s`, `m`, `i`, `x`, `q`. Any other character
133 /// in the flags string will trigger this error.
134 #[error("Invalid regex flag option: {0}")]
135 InvalidFlagOption(char),
136}
137
138impl Display for RDFRegex {
139 /// Formats the regex in a compact notation showing pattern and flags.
140 ///
141 /// # Output format
142 /// - `/pattern/flags`
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 write!(f, "/{}/{}", self.regex.as_str(), self.flags.as_deref().unwrap_or(""))
145 }
146}